@i14ks/ccv 0.13.0 → 0.15.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.
- package/bundle/ccv.mjs +1843 -195
- package/package.json +1 -1
- package/web/assets/antigravity-logo-DRKQga2U.png +0 -0
- package/web/assets/{codemirror-DAgbW3uz.js → codemirror-wcb7BToF.js} +1 -1
- package/web/assets/{index-CBTNRCgC.js → index-B1iGktMS.js} +1 -1
- package/web/assets/{index-Bjh8yEB4.js → index-B7uOCyzk.js} +1 -1
- package/web/assets/{index-CquqZqWN.js → index-B8ioctog.js} +1 -1
- package/web/assets/index-BKeYZb3W.css +1 -0
- package/web/assets/{index-CFzKsiua.js → index-BebgK8qS.js} +1 -1
- package/web/assets/{index-DDJ5NYOk.js → index-BgYZP6B5.js} +1 -1
- package/web/assets/{index-DPG5pWPG.js → index-BoWGjyq1.js} +1 -1
- package/web/assets/{index-C11bbOpw.js → index-Bug5lJuD.js} +1 -1
- package/web/assets/{index-1j2ILa-W.js → index-CKo4-duU.js} +1 -1
- package/web/assets/{index-XkZY-Wpw.js → index-CLUF7ja0.js} +1 -1
- package/web/assets/{index-C0igZOa9.js → index-D8i-20tT.js} +1 -1
- package/web/assets/{index-DmfDW_VA.js → index-DaeBIEG7.js} +1 -1
- package/web/assets/index-DfzFdzpd.js +134 -0
- package/web/assets/{index-BTNpZcr3.js → index-DzuxHBqN.js} +1 -1
- package/web/assets/{index-CIpZI228.js → index-H1KO3hWw.js} +1 -1
- package/web/assets/{index-Cd-2HW_i.js → index-ZyET8YfL.js} +1 -1
- package/web/assets/{index-BO3Em_j_.js → index-ggJz6TAQ.js} +1 -1
- package/web/assets/{index-CzYpffog.js → index-jRANpi-W.js} +1 -1
- package/web/assets/{index-BI8B49yl.js → index-qJpQucwj.js} +1 -1
- package/web/assets/{index-D-pPtz2I.js → index-s31Rk-Wi.js} +1 -1
- package/web/assets/{index-B_KTK-Ue.js → index-tW3KsNuy.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-CWCyiPjz.js +0 -132
- package/web/assets/index-s-BhjU3m.css +0 -1
package/bundle/ccv.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// dist/ccv.js
|
|
4
|
-
import { spawn as
|
|
5
|
-
import { readFile as
|
|
6
|
-
import
|
|
4
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5
|
+
import { readFile as readFile17 } from "node:fs/promises";
|
|
6
|
+
import path24 from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
@@ -146,9 +146,9 @@ function envDefaults() {
|
|
|
146
146
|
// ../host/dist/server.js
|
|
147
147
|
import { createServer } from "node:http";
|
|
148
148
|
import { createReadStream, existsSync as existsSync4, statSync as statSync3 } from "node:fs";
|
|
149
|
-
import
|
|
149
|
+
import path23 from "node:path";
|
|
150
150
|
import { homedir as homedir6 } from "node:os";
|
|
151
|
-
import { randomUUID as
|
|
151
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
152
152
|
import { WebSocketServer } from "ws";
|
|
153
153
|
import { renameSession as renameSession2 } from "@anthropic-ai/claude-agent-sdk";
|
|
154
154
|
|
|
@@ -164,8 +164,6 @@ function workspaceIdFromPath(absolutePath) {
|
|
|
164
164
|
// ../host/dist/session.js
|
|
165
165
|
import { EventEmitter } from "node:events";
|
|
166
166
|
import { randomUUID } from "node:crypto";
|
|
167
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
168
|
-
import { homedir as homedir3 } from "node:os";
|
|
169
167
|
import path5 from "node:path";
|
|
170
168
|
import { query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
171
169
|
|
|
@@ -1111,7 +1109,9 @@ function previewAnnotations(questions, answers) {
|
|
|
1111
1109
|
return annotations;
|
|
1112
1110
|
}
|
|
1113
1111
|
var USAGE_MIN_INTERVAL_MS = 2e4;
|
|
1114
|
-
var
|
|
1112
|
+
var HANDOFF_TOOL_DENIAL = "\u041B\u0438\u043C\u0438\u0442 \u043F\u043B\u0430\u043D\u0430 \u043F\u043E\u0447\u0442\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D, \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u044B. \u041E\u0442\u0432\u0435\u0442\u044C \u0445\u044D\u043D\u0434\u043E\u0444\u0444\u043E\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C, \u0431\u0435\u0437 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432.";
|
|
1113
|
+
var USAGE_TURN_INTERVAL_MS = 3e4;
|
|
1114
|
+
var INTERRUPT_SETTLE_MS = 15e3;
|
|
1115
1115
|
var CONTEXT_MIN_INTERVAL_MS = 5e3;
|
|
1116
1116
|
var ClaudeSession = class extends EventEmitter {
|
|
1117
1117
|
id;
|
|
@@ -1201,6 +1201,11 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1201
1201
|
#titleIsDefault = false;
|
|
1202
1202
|
/** Настоящий id сессии Claude Code — известен только после system/init. */
|
|
1203
1203
|
#claudeSessionId = null;
|
|
1204
|
+
/**
|
|
1205
|
+
* Идёт ход хэндоффа (`runHandoff`): любой вызов инструмента отклоняется —
|
|
1206
|
+
* и хуком, и `canUseTool`, в любом режиме прав.
|
|
1207
|
+
*/
|
|
1208
|
+
#handoffMode = false;
|
|
1204
1209
|
/**
|
|
1205
1210
|
* Имя, которое пользователь задал, но записать на диск ещё не удалось.
|
|
1206
1211
|
*
|
|
@@ -1329,6 +1334,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1329
1334
|
summary() {
|
|
1330
1335
|
return {
|
|
1331
1336
|
id: this.id,
|
|
1337
|
+
provider: "claude",
|
|
1332
1338
|
claudeSessionId: this.#claudeSessionId,
|
|
1333
1339
|
workspaceId: this.workspaceId,
|
|
1334
1340
|
cwd: this.cwd,
|
|
@@ -1407,7 +1413,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1407
1413
|
{
|
|
1408
1414
|
matcher: WRITE_TOOLS,
|
|
1409
1415
|
hooks: [(input) => this.#guardWrite(input)]
|
|
1410
|
-
}
|
|
1416
|
+
},
|
|
1417
|
+
// Без matcher — на все инструменты, включая MCP и инструменты
|
|
1418
|
+
// субагентов: в ходе хэндоффа отклоняется всё.
|
|
1419
|
+
{ hooks: [async () => this.#guardHandoff()] }
|
|
1411
1420
|
]
|
|
1412
1421
|
},
|
|
1413
1422
|
stderr: (data) => this.emit("stderr", data)
|
|
@@ -1637,11 +1646,11 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1637
1646
|
// -------------------------------------------------------------------------
|
|
1638
1647
|
// Ввод от пользователя
|
|
1639
1648
|
// -------------------------------------------------------------------------
|
|
1640
|
-
sendPrompt(text, images) {
|
|
1649
|
+
sendPrompt(text, images, service = false) {
|
|
1641
1650
|
if (this.#closing)
|
|
1642
1651
|
throw new Error("session is closed");
|
|
1643
1652
|
this.#record({ kind: "user.message", text, images });
|
|
1644
|
-
if (!this.#title || this.#titleIsDefault) {
|
|
1653
|
+
if (!service && (!this.#title || this.#titleIsDefault)) {
|
|
1645
1654
|
this.#title = text.slice(0, 80);
|
|
1646
1655
|
this.#titleIsDefault = false;
|
|
1647
1656
|
}
|
|
@@ -1773,6 +1782,49 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1773
1782
|
this.#interruptRequested = true;
|
|
1774
1783
|
await this.#query?.interrupt();
|
|
1775
1784
|
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Ход хэндоффа у лимита: прервать текущий ход (вместе с субагентами), затем
|
|
1787
|
+
* отправить служебный `prompt` с запретом на любые инструменты и вернуть
|
|
1788
|
+
* текст ответа. `null` — хода не вышло: ошибка, пустой ответ или ход не
|
|
1789
|
+
* кончился за `timeoutMs`; тогда хэндофф собирают из журнала.
|
|
1790
|
+
*/
|
|
1791
|
+
async runHandoff(prompt, timeoutMs) {
|
|
1792
|
+
if (this.#closing)
|
|
1793
|
+
return null;
|
|
1794
|
+
this.#handoffMode = true;
|
|
1795
|
+
try {
|
|
1796
|
+
if (this.#state === "running" || this.#state === "waiting_permission") {
|
|
1797
|
+
const settled = this.#nextTurnEnd(INTERRUPT_SETTLE_MS);
|
|
1798
|
+
await this.interrupt().catch(() => void 0);
|
|
1799
|
+
await settled;
|
|
1800
|
+
}
|
|
1801
|
+
if (this.#closing || !this.#query || this.#state !== "idle")
|
|
1802
|
+
return null;
|
|
1803
|
+
const reply = this.#nextTurnEnd(timeoutMs);
|
|
1804
|
+
this.sendPrompt(prompt, void 0, true);
|
|
1805
|
+
const turn = await reply;
|
|
1806
|
+
const text = turn?.text?.trim();
|
|
1807
|
+
return turn && !turn.isError && text ? text : null;
|
|
1808
|
+
} catch {
|
|
1809
|
+
return null;
|
|
1810
|
+
} finally {
|
|
1811
|
+
this.#handoffMode = false;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
/** Ближайший конец хода (`result`) или `null`, если не дождались. */
|
|
1815
|
+
#nextTurnEnd(timeoutMs) {
|
|
1816
|
+
return new Promise((resolve) => {
|
|
1817
|
+
const onEnd = (turn) => {
|
|
1818
|
+
clearTimeout(timer);
|
|
1819
|
+
resolve(turn);
|
|
1820
|
+
};
|
|
1821
|
+
const timer = setTimeout(() => {
|
|
1822
|
+
this.off("turnEnd", onEnd);
|
|
1823
|
+
resolve(null);
|
|
1824
|
+
}, timeoutMs);
|
|
1825
|
+
this.once("turnEnd", onEnd);
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1776
1828
|
/**
|
|
1777
1829
|
* Смена режима прав на лету.
|
|
1778
1830
|
*
|
|
@@ -1867,7 +1919,6 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1867
1919
|
const raw = await q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET();
|
|
1868
1920
|
this.#usage = toUsageState(raw);
|
|
1869
1921
|
this.emit("usage", this.#usage);
|
|
1870
|
-
writeFinisherSnapshot(this.#usage, this.#claudeSessionId, this.cwd);
|
|
1871
1922
|
} catch {
|
|
1872
1923
|
} finally {
|
|
1873
1924
|
this.#usageAt = Date.now();
|
|
@@ -2036,6 +2087,18 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2036
2087
|
}
|
|
2037
2088
|
};
|
|
2038
2089
|
};
|
|
2090
|
+
#guardHandoff() {
|
|
2091
|
+
if (!this.#handoffMode)
|
|
2092
|
+
return { continue: true };
|
|
2093
|
+
return {
|
|
2094
|
+
continue: true,
|
|
2095
|
+
hookSpecificOutput: {
|
|
2096
|
+
hookEventName: "PreToolUse",
|
|
2097
|
+
permissionDecision: "deny",
|
|
2098
|
+
permissionDecisionReason: HANDOFF_TOOL_DENIAL
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2039
2102
|
/**
|
|
2040
2103
|
* Мост между `canUseTool` из SDK и решением человека в UI.
|
|
2041
2104
|
*
|
|
@@ -2053,6 +2116,9 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2053
2116
|
* это и происходило, пока вопросы показывались обычной карточкой прав.
|
|
2054
2117
|
*/
|
|
2055
2118
|
#canUseTool = (toolName, input, { signal, decisionReason, toolUseID }) => {
|
|
2119
|
+
if (this.#handoffMode) {
|
|
2120
|
+
return Promise.resolve({ behavior: "deny", message: HANDOFF_TOOL_DENIAL });
|
|
2121
|
+
}
|
|
2056
2122
|
if (this.#options.readOnly) {
|
|
2057
2123
|
this.#record({
|
|
2058
2124
|
kind: "error",
|
|
@@ -2202,6 +2268,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2202
2268
|
contextTokens: this.#contextTokens,
|
|
2203
2269
|
text: "result" in message ? message.result : null
|
|
2204
2270
|
});
|
|
2271
|
+
this.emit("turnEnd", {
|
|
2272
|
+
text: "result" in message ? message.result : null,
|
|
2273
|
+
isError: message.is_error
|
|
2274
|
+
});
|
|
2205
2275
|
if (this.#options.outputFormat) {
|
|
2206
2276
|
this.emit("structured", {
|
|
2207
2277
|
data: "structured_output" in message ? message.structured_output : void 0,
|
|
@@ -2427,60 +2497,1178 @@ function toUsageState(raw) {
|
|
|
2427
2497
|
fetchedAt: Date.now()
|
|
2428
2498
|
};
|
|
2429
2499
|
}
|
|
2430
|
-
function
|
|
2431
|
-
if (!
|
|
2432
|
-
return;
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2500
|
+
function toUsageWindow(raw) {
|
|
2501
|
+
if (!raw || typeof raw.utilization !== "number")
|
|
2502
|
+
return null;
|
|
2503
|
+
return {
|
|
2504
|
+
// Доля окна и так приходит в 0–100, но полоса шире дорожки — это уже не
|
|
2505
|
+
// «почти всё израсходовано», а сломанная вёрстка.
|
|
2506
|
+
utilization: Math.min(100, Math.max(0, raw.utilization)),
|
|
2507
|
+
resetsAt: typeof raw.resets_at === "string" ? raw.resets_at : ""
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
function contentToText2(content) {
|
|
2511
|
+
if (typeof content === "string")
|
|
2512
|
+
return content;
|
|
2513
|
+
if (!Array.isArray(content))
|
|
2514
|
+
return "";
|
|
2515
|
+
return content.map((block) => {
|
|
2516
|
+
if (typeof block === "string")
|
|
2517
|
+
return block;
|
|
2518
|
+
if (block && typeof block === "object" && "text" in block) {
|
|
2519
|
+
return String(block.text);
|
|
2520
|
+
}
|
|
2521
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
2522
|
+
return `[${String(block.type)}]`;
|
|
2523
|
+
}
|
|
2524
|
+
return "";
|
|
2525
|
+
}).join("");
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// ../host/dist/antigravity-session.js
|
|
2529
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
2530
|
+
import { spawn as spawn2, execFile as execFile3 } from "node:child_process";
|
|
2531
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2532
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2533
|
+
import { tmpdir } from "node:os";
|
|
2534
|
+
import path7 from "node:path";
|
|
2535
|
+
import { createInterface } from "node:readline";
|
|
2536
|
+
|
|
2537
|
+
// ../host/dist/antigravity-cli.js
|
|
2538
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2539
|
+
import { existsSync } from "node:fs";
|
|
2540
|
+
import { homedir as homedir3 } from "node:os";
|
|
2541
|
+
import path6 from "node:path";
|
|
2542
|
+
function agyExecutable() {
|
|
2543
|
+
const forced = process.env.CCV_AGY_PATH;
|
|
2544
|
+
if (forced && existsSync(forced))
|
|
2545
|
+
return forced;
|
|
2546
|
+
const names2 = process.platform === "win32" ? ["agy.exe", "agy"] : ["agy"];
|
|
2547
|
+
const dirs = (process.env.PATH ?? "").split(path6.delimiter).filter(Boolean);
|
|
2548
|
+
if (process.platform === "win32") {
|
|
2549
|
+
const local = process.env.LOCALAPPDATA ?? path6.join(homedir3(), "AppData", "Local");
|
|
2550
|
+
dirs.push(path6.join(local, "agy", "bin"));
|
|
2551
|
+
} else {
|
|
2552
|
+
dirs.push(path6.join(homedir3(), ".local", "bin"), "/usr/local/bin", "/opt/homebrew/bin");
|
|
2553
|
+
}
|
|
2554
|
+
for (const dir of dirs) {
|
|
2555
|
+
for (const name of names2) {
|
|
2556
|
+
const candidate = path6.join(dir, name);
|
|
2557
|
+
if (existsSync(candidate))
|
|
2558
|
+
return candidate;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
var AGY_MISSING_HINT = "Antigravity CLI (agy) \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 \u0435\u0433\u043E \u0441 https://antigravity.google \u0438 \u0432\u043E\u0439\u0434\u0438\u0442\u0435, \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0432 `agy` \u0432 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435, \u2014 \u0438\u043B\u0438 \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u0443\u0442\u044C \u0432 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439 CCV_AGY_PATH.";
|
|
2564
|
+
var modelsCache = null;
|
|
2565
|
+
function agyModels(file) {
|
|
2566
|
+
if (modelsCache)
|
|
2567
|
+
return modelsCache;
|
|
2568
|
+
const pending = new Promise((resolve) => {
|
|
2569
|
+
execFile2(file, ["models"], { timeout: 3e4, windowsHide: true }, (error, stdout) => {
|
|
2570
|
+
if (error) {
|
|
2571
|
+
resolve([]);
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
const models = [];
|
|
2575
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
2576
|
+
const [value, displayName] = line.split(" ");
|
|
2577
|
+
if (!value || !displayName || /\s/.test(value))
|
|
2578
|
+
continue;
|
|
2579
|
+
models.push({
|
|
2580
|
+
value,
|
|
2581
|
+
resolvedModel: value,
|
|
2582
|
+
displayName: displayName.trim(),
|
|
2583
|
+
description: "",
|
|
2584
|
+
supportsEffort: false,
|
|
2585
|
+
effortLevels: [],
|
|
2586
|
+
supportsThinking: false,
|
|
2587
|
+
supportsAutoMode: false
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
resolve(models);
|
|
2591
|
+
});
|
|
2592
|
+
});
|
|
2593
|
+
modelsCache = pending;
|
|
2594
|
+
void pending.then((models) => {
|
|
2595
|
+
if (models.length === 0)
|
|
2596
|
+
modelsCache = null;
|
|
2597
|
+
});
|
|
2598
|
+
return pending;
|
|
2599
|
+
}
|
|
2600
|
+
function agyCommand(file, command, cwd) {
|
|
2601
|
+
return new Promise((resolve) => {
|
|
2602
|
+
execFile2(file, ["--output-format", "json", "-p", command], {
|
|
2603
|
+
cwd,
|
|
2604
|
+
timeout: 6e4,
|
|
2605
|
+
windowsHide: true,
|
|
2606
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
2607
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
2608
|
+
}, (error, stdout) => {
|
|
2609
|
+
const line = stdout.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.startsWith("{")).pop();
|
|
2610
|
+
if (line) {
|
|
2611
|
+
try {
|
|
2612
|
+
resolve(JSON.parse(line));
|
|
2613
|
+
return;
|
|
2614
|
+
} catch {
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
resolve({ status: "ERROR", error: error ? error.message : "agy \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B" });
|
|
2618
|
+
});
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
var USAGE_TTL_MS = 3e4;
|
|
2622
|
+
var usageCache = null;
|
|
2623
|
+
var usageInFlight = null;
|
|
2624
|
+
var usageError = null;
|
|
2625
|
+
function agyUsageError() {
|
|
2626
|
+
return usageError;
|
|
2627
|
+
}
|
|
2628
|
+
function agyUsage(file, force = false) {
|
|
2629
|
+
if (!force && usageCache && Date.now() - usageCache.at < USAGE_TTL_MS) {
|
|
2630
|
+
return Promise.resolve(usageCache.value);
|
|
2631
|
+
}
|
|
2632
|
+
if (usageInFlight)
|
|
2633
|
+
return usageInFlight;
|
|
2634
|
+
usageInFlight = agyCommand(file, "/usage").then((reply) => {
|
|
2635
|
+
const value = parseUsage(reply);
|
|
2636
|
+
if (value)
|
|
2637
|
+
usageCache = { at: Date.now(), value };
|
|
2638
|
+
usageError = value ? null : reply.error || "agy \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u043A\u0432\u043E\u0442\u0443";
|
|
2639
|
+
return value;
|
|
2640
|
+
}).finally(() => {
|
|
2641
|
+
usageInFlight = null;
|
|
2642
|
+
});
|
|
2643
|
+
return usageInFlight;
|
|
2644
|
+
}
|
|
2645
|
+
var PERIOD_LABELS = {
|
|
2646
|
+
weekly: "\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442",
|
|
2647
|
+
daily: "\u0434\u043D\u0435\u0432\u043D\u043E\u0439 \u043B\u0438\u043C\u0438\u0442",
|
|
2648
|
+
monthly: "\u043C\u0435\u0441\u044F\u0447\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442"
|
|
2649
|
+
};
|
|
2650
|
+
function parseUsage(reply) {
|
|
2651
|
+
if (reply.status !== "SUCCESS")
|
|
2652
|
+
return null;
|
|
2653
|
+
const data = reply.command?.data;
|
|
2654
|
+
if (!data || !Array.isArray(data.groups))
|
|
2655
|
+
return null;
|
|
2656
|
+
const buckets = [];
|
|
2657
|
+
for (const group of data.groups) {
|
|
2658
|
+
const groupName = group.name ?? "";
|
|
2659
|
+
for (const bucket of group.buckets ?? []) {
|
|
2660
|
+
if (typeof bucket.remaining_fraction !== "number")
|
|
2661
|
+
continue;
|
|
2662
|
+
const used = Math.min(100, Math.max(0, (1 - bucket.remaining_fraction) * 100));
|
|
2663
|
+
buckets.push({
|
|
2664
|
+
id: bucket.id ?? `${groupName}:${bucket.name ?? ""}`,
|
|
2665
|
+
label: shortGroupLabel(groupName),
|
|
2666
|
+
// Имя окна у `agy` — «Weekly Limit Remaining», а полоса показывает
|
|
2667
|
+
// израсходованное: подпись с «Remaining» читалась бы наоборот.
|
|
2668
|
+
title: [groupName, PERIOD_LABELS[bucket.window ?? ""] ?? bucket.name].filter(Boolean).join(" \u2014 "),
|
|
2669
|
+
description: group.description ?? "",
|
|
2670
|
+
period: bucket.window ?? "",
|
|
2671
|
+
window: { utilization: used, resetsAt: bucket.reset_time ?? "" }
|
|
2672
|
+
});
|
|
2444
2673
|
}
|
|
2674
|
+
}
|
|
2675
|
+
return {
|
|
2676
|
+
available: buckets.length > 0,
|
|
2677
|
+
plan: "",
|
|
2678
|
+
fiveHour: null,
|
|
2679
|
+
sevenDay: null,
|
|
2680
|
+
sevenDayOpus: null,
|
|
2681
|
+
buckets,
|
|
2682
|
+
fetchedAt: Date.now()
|
|
2445
2683
|
};
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2684
|
+
}
|
|
2685
|
+
function shortGroupLabel(name) {
|
|
2686
|
+
const short = name.replace(/\s+models?$/i, "").replace(/\s+and\s+/gi, "/").trim();
|
|
2687
|
+
return short || name;
|
|
2688
|
+
}
|
|
2689
|
+
var INTERACTIVE_COMMANDS = /* @__PURE__ */ new Set(["config", "hooks", "permissions", "context"]);
|
|
2690
|
+
var AGY_CLI_COMMANDS = /* @__PURE__ */ new Set([
|
|
2691
|
+
"help",
|
|
2692
|
+
"usage",
|
|
2693
|
+
"quota",
|
|
2694
|
+
"skills",
|
|
2695
|
+
"agents",
|
|
2696
|
+
"changelog",
|
|
2697
|
+
"credits",
|
|
2698
|
+
"model",
|
|
2699
|
+
"effort"
|
|
2700
|
+
]);
|
|
2701
|
+
var COMMAND_ARGS = { model: "[\u043C\u043E\u0434\u0435\u043B\u044C]", effort: "[low|medium|high|max]" };
|
|
2702
|
+
var catalogCache = null;
|
|
2703
|
+
function agyCommandCatalog(file, cwd) {
|
|
2704
|
+
if (catalogCache)
|
|
2705
|
+
return catalogCache;
|
|
2706
|
+
const pending = Promise.all([agyCommand(file, "/help", cwd), agyCommand(file, "/skills", cwd)]).then(([help, skills]) => {
|
|
2707
|
+
const commands = [];
|
|
2708
|
+
const helpData = help.command?.data;
|
|
2709
|
+
for (const c of helpData?.commands ?? []) {
|
|
2710
|
+
if (!c.name || INTERACTIVE_COMMANDS.has(c.name))
|
|
2711
|
+
continue;
|
|
2712
|
+
commands.push({
|
|
2713
|
+
name: c.name,
|
|
2714
|
+
description: c.description ?? "",
|
|
2715
|
+
argumentHint: COMMAND_ARGS[c.name] ?? "",
|
|
2716
|
+
aliases: c.aliases ?? []
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
const skillsData = skills.command?.data;
|
|
2720
|
+
for (const s of skillsData?.skills ?? []) {
|
|
2721
|
+
if (!s.name || commands.some((c) => c.name === s.name))
|
|
2722
|
+
continue;
|
|
2723
|
+
commands.push({ name: s.name, description: s.description ?? "", argumentHint: "", aliases: [] });
|
|
2724
|
+
}
|
|
2725
|
+
return commands;
|
|
2726
|
+
});
|
|
2727
|
+
catalogCache = pending;
|
|
2728
|
+
void pending.then((commands) => {
|
|
2729
|
+
if (commands.length === 0)
|
|
2730
|
+
catalogCache = null;
|
|
2731
|
+
});
|
|
2732
|
+
return pending;
|
|
2733
|
+
}
|
|
2734
|
+
function agyContextWindow(model) {
|
|
2735
|
+
if (!model)
|
|
2736
|
+
return null;
|
|
2737
|
+
const id = model.toLowerCase();
|
|
2738
|
+
if (id.startsWith("gemini-"))
|
|
2739
|
+
return 1048576;
|
|
2740
|
+
if (id.startsWith("claude-"))
|
|
2741
|
+
return 2e5;
|
|
2742
|
+
if (id.startsWith("gpt-oss"))
|
|
2743
|
+
return 131072;
|
|
2744
|
+
return null;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// ../host/dist/antigravity-session.js
|
|
2748
|
+
var TRANSIENT = /* @__PURE__ */ new Set(["assistant.delta", "thinking.delta", "state"]);
|
|
2749
|
+
var AntigravitySession = class extends EventEmitter2 {
|
|
2750
|
+
id;
|
|
2751
|
+
workspaceId;
|
|
2752
|
+
cwd;
|
|
2753
|
+
journal = new Journal();
|
|
2754
|
+
provider = "antigravity";
|
|
2755
|
+
#store;
|
|
2756
|
+
#record;
|
|
2757
|
+
#history = Promise.resolve([]);
|
|
2758
|
+
#child = null;
|
|
2759
|
+
#executable = null;
|
|
2760
|
+
#state = "starting";
|
|
2761
|
+
#lastTurn = null;
|
|
2762
|
+
#capabilities = null;
|
|
2763
|
+
#titleIsDefault = false;
|
|
2764
|
+
#closing = false;
|
|
2765
|
+
/** Ходы, отправленные процессу и ещё не закрытые его `result`. */
|
|
2766
|
+
#pendingTurns = 0;
|
|
2767
|
+
/** Перезапуск (смена модели/режима), отложенный до конца хода. */
|
|
2768
|
+
#restartPending = false;
|
|
2769
|
+
#interrupting = false;
|
|
2770
|
+
/** Накопленный текст шагов-ответов: на `DONE` уходит одним `assistant.text`. */
|
|
2771
|
+
#texts = /* @__PURE__ */ new Map();
|
|
2772
|
+
#thinking = /* @__PURE__ */ new Map();
|
|
2773
|
+
/** Шаги-инструменты, о которых уже сказано `tool.use`. */
|
|
2774
|
+
#tools = /* @__PURE__ */ new Set();
|
|
2775
|
+
#contextTokens = null;
|
|
2776
|
+
/** Модель, на которой `agy` реально работает (`init.model`) — с учётом его умолчания. */
|
|
2777
|
+
#activeModel = null;
|
|
2778
|
+
#usage = null;
|
|
2779
|
+
#context = null;
|
|
2780
|
+
#commands = [];
|
|
2781
|
+
#stderrTail = "";
|
|
2782
|
+
#unsaved = [];
|
|
2783
|
+
#flushTimer = null;
|
|
2784
|
+
#updatedAt = Date.now();
|
|
2785
|
+
constructor(options2) {
|
|
2786
|
+
super();
|
|
2787
|
+
this.#store = options2.store;
|
|
2788
|
+
this.workspaceId = options2.workspaceId;
|
|
2789
|
+
this.cwd = options2.cwd;
|
|
2790
|
+
const now = Date.now();
|
|
2791
|
+
this.#record = options2.record ? { ...options2.record } : {
|
|
2792
|
+
id: randomUUID2(),
|
|
2793
|
+
conversationId: null,
|
|
2794
|
+
workspaceId: options2.workspaceId,
|
|
2795
|
+
cwd: options2.cwd,
|
|
2796
|
+
title: options2.defaultTitle ?? null,
|
|
2797
|
+
model: options2.model ?? null,
|
|
2798
|
+
permissionMode: options2.permissionMode ?? "default",
|
|
2799
|
+
effort: null,
|
|
2800
|
+
createdAt: now,
|
|
2801
|
+
updatedAt: now
|
|
2802
|
+
};
|
|
2803
|
+
if (!options2.record)
|
|
2804
|
+
this.#titleIsDefault = options2.defaultTitle !== void 0;
|
|
2805
|
+
if (options2.record && options2.permissionMode) {
|
|
2806
|
+
this.#record.permissionMode = options2.permissionMode;
|
|
2807
|
+
}
|
|
2808
|
+
this.id = this.#record.id;
|
|
2809
|
+
this.#updatedAt = this.#record.updatedAt;
|
|
2810
|
+
}
|
|
2811
|
+
// -------------------------------------------------------------------------
|
|
2812
|
+
// Публичное состояние — та же форма, что у ClaudeSession
|
|
2813
|
+
// -------------------------------------------------------------------------
|
|
2814
|
+
get state() {
|
|
2815
|
+
return this.#state;
|
|
2816
|
+
}
|
|
2817
|
+
get capabilities() {
|
|
2818
|
+
return this.#capabilities;
|
|
2819
|
+
}
|
|
2820
|
+
/** Постоянный id сессии: под ним она в архиве, закреплённых и папках. */
|
|
2821
|
+
get claudeSessionId() {
|
|
2822
|
+
return this.id;
|
|
2823
|
+
}
|
|
2824
|
+
get parentSessionId() {
|
|
2825
|
+
return null;
|
|
2826
|
+
}
|
|
2827
|
+
get usage() {
|
|
2828
|
+
return this.#usage;
|
|
2829
|
+
}
|
|
2830
|
+
get context() {
|
|
2831
|
+
return this.#context;
|
|
2832
|
+
}
|
|
2833
|
+
history() {
|
|
2834
|
+
return this.#history;
|
|
2835
|
+
}
|
|
2836
|
+
summary() {
|
|
2837
|
+
return {
|
|
2838
|
+
id: this.id,
|
|
2839
|
+
provider: "antigravity",
|
|
2840
|
+
claudeSessionId: this.id,
|
|
2841
|
+
workspaceId: this.workspaceId,
|
|
2842
|
+
cwd: this.cwd,
|
|
2843
|
+
parentSessionId: null,
|
|
2844
|
+
title: this.#record.title,
|
|
2845
|
+
state: this.#state,
|
|
2846
|
+
lastTurn: this.#lastTurn,
|
|
2847
|
+
backgroundWork: false,
|
|
2848
|
+
model: this.#record.model,
|
|
2849
|
+
permissionMode: this.#record.permissionMode,
|
|
2850
|
+
effort: this.#record.effort,
|
|
2851
|
+
ultracode: false,
|
|
2852
|
+
thinking: true,
|
|
2853
|
+
switchModelsOnFlag: true,
|
|
2854
|
+
createdAt: this.#record.createdAt,
|
|
2855
|
+
updatedAt: this.#updatedAt,
|
|
2856
|
+
lastSeq: this.journal.lastSeq,
|
|
2857
|
+
totalCostUsd: 0
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
// -------------------------------------------------------------------------
|
|
2861
|
+
// Жизненный цикл
|
|
2862
|
+
// -------------------------------------------------------------------------
|
|
2863
|
+
start() {
|
|
2864
|
+
if (this.#record.conversationId) {
|
|
2865
|
+
this.#history = this.#store.readEvents(this.id);
|
|
2866
|
+
void this.#history.then((entries) => {
|
|
2867
|
+
if (this.#contextTokens !== null)
|
|
2868
|
+
return;
|
|
2869
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
2870
|
+
const event = entries[i].event;
|
|
2871
|
+
if (event.kind === "result" && event.contextTokens !== null) {
|
|
2872
|
+
this.#contextTokens = event.contextTokens;
|
|
2873
|
+
this.#emitContext();
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
void this.#store.save(this.#record);
|
|
2880
|
+
this.#executable = agyExecutable();
|
|
2881
|
+
if (!this.#executable) {
|
|
2882
|
+
this.#record_({ kind: "error", message: AGY_MISSING_HINT });
|
|
2883
|
+
this.#setState("error");
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
this.#capabilities = this.#baseCapabilities();
|
|
2887
|
+
this.emit("capabilities");
|
|
2888
|
+
void agyModels(this.#executable).then((models) => {
|
|
2889
|
+
if (!this.#capabilities || models.length === 0)
|
|
2890
|
+
return;
|
|
2891
|
+
this.#capabilities = { ...this.#capabilities, models };
|
|
2892
|
+
this.emit("capabilities");
|
|
2893
|
+
});
|
|
2894
|
+
void agyCommandCatalog(this.#executable, this.cwd).then((commands) => {
|
|
2895
|
+
if (!this.#capabilities || commands.length === 0)
|
|
2896
|
+
return;
|
|
2897
|
+
this.#commands = commands;
|
|
2898
|
+
this.#capabilities = this.#baseCapabilities();
|
|
2899
|
+
this.emit("capabilities");
|
|
2900
|
+
});
|
|
2901
|
+
void this.refreshUsage();
|
|
2902
|
+
this.#spawn();
|
|
2903
|
+
}
|
|
2904
|
+
#baseCapabilities() {
|
|
2905
|
+
return {
|
|
2906
|
+
cwd: this.cwd,
|
|
2907
|
+
model: this.#record.model ?? this.#activeModel ?? "",
|
|
2908
|
+
permissionMode: this.#record.permissionMode,
|
|
2909
|
+
slashCommands: this.#commands.map((c) => c.name),
|
|
2910
|
+
commands: this.#commands,
|
|
2911
|
+
models: this.#capabilities?.models ?? [],
|
|
2912
|
+
tools: this.#capabilities?.tools ?? [],
|
|
2913
|
+
agents: [],
|
|
2914
|
+
mcpServers: [],
|
|
2915
|
+
flags: []
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
#args() {
|
|
2919
|
+
const args = ["--input-format", "stream-json", "--output-format", "stream-json"];
|
|
2920
|
+
switch (this.#record.permissionMode) {
|
|
2921
|
+
case "acceptEdits":
|
|
2922
|
+
args.push("--mode", "accept-edits");
|
|
2923
|
+
break;
|
|
2924
|
+
case "plan":
|
|
2925
|
+
args.push("--mode", "plan");
|
|
2926
|
+
break;
|
|
2927
|
+
case "bypassPermissions":
|
|
2928
|
+
args.push("--dangerously-skip-permissions");
|
|
2929
|
+
break;
|
|
2930
|
+
default:
|
|
2931
|
+
break;
|
|
2932
|
+
}
|
|
2933
|
+
if (this.#record.model)
|
|
2934
|
+
args.push("--model", this.#record.model);
|
|
2935
|
+
const effort = agyEffort(this.#record.effort);
|
|
2936
|
+
if (effort)
|
|
2937
|
+
args.push("--effort", effort);
|
|
2938
|
+
if (this.#record.conversationId)
|
|
2939
|
+
args.push("--conversation", this.#record.conversationId);
|
|
2940
|
+
args.push("-p=");
|
|
2941
|
+
return args;
|
|
2942
|
+
}
|
|
2943
|
+
#spawn() {
|
|
2944
|
+
if (!this.#executable || this.#closing)
|
|
2945
|
+
return;
|
|
2946
|
+
let child;
|
|
2947
|
+
try {
|
|
2948
|
+
child = spawn2(this.#executable, this.#args(), {
|
|
2949
|
+
cwd: this.cwd,
|
|
2950
|
+
windowsHide: true,
|
|
2951
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
2952
|
+
});
|
|
2953
|
+
} catch (error) {
|
|
2954
|
+
this.#record_({ kind: "error", message: `\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C agy: ${describe2(error)}` });
|
|
2955
|
+
this.#setState("error");
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
this.#child = child;
|
|
2959
|
+
this.#stderrTail = "";
|
|
2960
|
+
this.#texts.clear();
|
|
2961
|
+
this.#thinking.clear();
|
|
2962
|
+
this.#tools.clear();
|
|
2963
|
+
const lines = createInterface({ input: child.stdout });
|
|
2964
|
+
lines.on("line", (line) => {
|
|
2965
|
+
if (this.#child !== child)
|
|
2966
|
+
return;
|
|
2967
|
+
this.#handleLine(line);
|
|
2968
|
+
});
|
|
2969
|
+
child.stderr.setEncoding("utf8");
|
|
2970
|
+
child.stderr.on("data", (data) => {
|
|
2971
|
+
this.#stderrTail = (this.#stderrTail + data).slice(-4e3);
|
|
2972
|
+
this.emit("stderr", data);
|
|
2973
|
+
});
|
|
2974
|
+
child.stdin.on("error", () => void 0);
|
|
2975
|
+
child.on("error", (error) => {
|
|
2976
|
+
if (this.#child !== child)
|
|
2977
|
+
return;
|
|
2978
|
+
this.#record_({ kind: "error", message: `agy: ${describe2(error)}` });
|
|
2979
|
+
});
|
|
2980
|
+
child.on("exit", (code) => {
|
|
2981
|
+
if (this.#child !== child)
|
|
2982
|
+
return;
|
|
2983
|
+
this.#child = null;
|
|
2984
|
+
this.#onExit(code);
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
/** Процесс ушёл сам — не по нашей просьбе. */
|
|
2988
|
+
#onExit(code) {
|
|
2989
|
+
if (this.#closing)
|
|
2990
|
+
return;
|
|
2991
|
+
const busy = this.#pendingTurns > 0;
|
|
2992
|
+
this.#pendingTurns = 0;
|
|
2993
|
+
if (code === 0 && !busy) {
|
|
2994
|
+
this.#setState("idle");
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
const tail = this.#stderrTail.trim().split(/\r?\n/).slice(-3).join("\n");
|
|
2998
|
+
this.#lastTurn = busy ? "failed" : this.#lastTurn;
|
|
2999
|
+
this.#record_({
|
|
3000
|
+
kind: "error",
|
|
3001
|
+
message: `Antigravity CLI \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F (\u043A\u043E\u0434 ${code ?? "\u2014"})${tail ? `: ${tail}` : ""}`
|
|
3002
|
+
});
|
|
3003
|
+
this.#setState("error");
|
|
3004
|
+
}
|
|
3005
|
+
async close(_reason = "closed by client") {
|
|
3006
|
+
this.#closing = true;
|
|
3007
|
+
this.#kill();
|
|
3008
|
+
this.#flush();
|
|
3009
|
+
this.#setState("closed");
|
|
3010
|
+
}
|
|
3011
|
+
#kill() {
|
|
3012
|
+
const child = this.#child;
|
|
3013
|
+
this.#child = null;
|
|
3014
|
+
if (!child || child.exitCode !== null)
|
|
3015
|
+
return;
|
|
3016
|
+
try {
|
|
3017
|
+
child.stdin.end();
|
|
3018
|
+
} catch {
|
|
3019
|
+
}
|
|
3020
|
+
if (process.platform === "win32" && child.pid) {
|
|
3021
|
+
execFile3("taskkill", ["/pid", String(child.pid), "/T", "/F"], { windowsHide: true }, () => {
|
|
3022
|
+
if (child.exitCode === null)
|
|
3023
|
+
child.kill();
|
|
3024
|
+
});
|
|
3025
|
+
} else {
|
|
3026
|
+
child.kill("SIGTERM");
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
/** Новый процесс на том же разговоре — смена настроек, ручная починка. */
|
|
3030
|
+
#restart() {
|
|
3031
|
+
if (this.#closing)
|
|
3032
|
+
return;
|
|
3033
|
+
this.#restartPending = false;
|
|
3034
|
+
this.#kill();
|
|
3035
|
+
this.#pendingTurns = 0;
|
|
3036
|
+
this.#spawn();
|
|
3037
|
+
if (this.#state !== "error" && this.#state !== "closed")
|
|
3038
|
+
this.#setState("idle");
|
|
3039
|
+
}
|
|
3040
|
+
reconnect(_reason) {
|
|
3041
|
+
this.#restart();
|
|
3042
|
+
}
|
|
3043
|
+
// -------------------------------------------------------------------------
|
|
3044
|
+
// Ввод
|
|
3045
|
+
// -------------------------------------------------------------------------
|
|
3046
|
+
sendPrompt(text, images) {
|
|
3047
|
+
if (this.#closing)
|
|
3048
|
+
throw new Error("session is closed");
|
|
3049
|
+
const command = /^\/([\w-]+)(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
3050
|
+
if (command && AGY_CLI_COMMANDS.has(command[1]) && (!images || images.length === 0)) {
|
|
3051
|
+
void this.#runCliCommand(text, command[1], command[2]?.trim() ?? "");
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
this.#record_({ kind: "user.message", text, images });
|
|
3055
|
+
if (!this.#record.title || this.#titleIsDefault) {
|
|
3056
|
+
this.#record.title = text.slice(0, 80);
|
|
3057
|
+
this.#titleIsDefault = false;
|
|
3058
|
+
}
|
|
3059
|
+
this.#lastTurn = null;
|
|
3060
|
+
if (!this.#child)
|
|
3061
|
+
this.#spawn();
|
|
3062
|
+
if (!this.#child)
|
|
3063
|
+
return;
|
|
3064
|
+
this.#pendingTurns += 1;
|
|
3065
|
+
this.#setState("running");
|
|
3066
|
+
const child = this.#child;
|
|
3067
|
+
void this.#composePrompt(text, images).then((prompt) => {
|
|
3068
|
+
if (this.#child !== child)
|
|
3069
|
+
return;
|
|
3070
|
+
const line = JSON.stringify({
|
|
3071
|
+
event: "user",
|
|
3072
|
+
message: { role: "user", content: [{ type: "text", text: prompt }] }
|
|
3073
|
+
});
|
|
3074
|
+
child.stdin.write(line + "\n");
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Встроенная команда `agy` — отдельным вызовом, без хода модели.
|
|
3079
|
+
*
|
|
3080
|
+
* `/model` и `/effort` с аргументом меняют настройки сессии ровно как
|
|
3081
|
+
* дропдауны (своё значение у `agy -p` прожило бы один процесс), без
|
|
3082
|
+
* аргумента — называют текущее. `/usage` заодно обновляет шкалу в шапке.
|
|
3083
|
+
* Остальное — как ответил сам `agy`. Посреди хода команда отвечает в ленту,
|
|
3084
|
+
* но ни состояния, ни `result` не трогает: ход закроет его собственный.
|
|
3085
|
+
*/
|
|
3086
|
+
async #runCliCommand(text, name, arg) {
|
|
3087
|
+
this.#record_({ kind: "user.message", text });
|
|
3088
|
+
const turnRunning = this.#pendingTurns > 0;
|
|
3089
|
+
if (!turnRunning) {
|
|
3090
|
+
this.#lastTurn = null;
|
|
3091
|
+
this.#setState("running");
|
|
3092
|
+
}
|
|
3093
|
+
const started = Date.now();
|
|
3094
|
+
let reply = null;
|
|
3095
|
+
let error = null;
|
|
3096
|
+
try {
|
|
3097
|
+
if (!this.#executable) {
|
|
3098
|
+
error = AGY_MISSING_HINT;
|
|
3099
|
+
} else if (name === "model") {
|
|
3100
|
+
({ reply, error } = await this.#modelCommand(arg));
|
|
3101
|
+
} else if (name === "effort") {
|
|
3102
|
+
({ reply, error } = await this.#effortCommand(arg));
|
|
3103
|
+
} else if (name === "usage" || name === "quota") {
|
|
3104
|
+
const usage = await agyUsage(this.#executable, true);
|
|
3105
|
+
if (usage) {
|
|
3106
|
+
this.#usage = usage;
|
|
3107
|
+
this.emit("usage", usage);
|
|
3108
|
+
reply = formatUsage(usage);
|
|
3109
|
+
} else {
|
|
3110
|
+
error = `agy \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u043A\u0432\u043E\u0442\u0443 (\`/usage\`): ${agyUsageError() ?? "\u043D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430"}`;
|
|
3111
|
+
}
|
|
3112
|
+
} else {
|
|
3113
|
+
const result = await agyCommand(this.#executable, `/${name}${arg ? ` ${arg}` : ""}`, this.cwd);
|
|
3114
|
+
if (result.status === "SUCCESS")
|
|
3115
|
+
reply = formatCommandReply(name, result.response ?? "");
|
|
3116
|
+
else
|
|
3117
|
+
error = result.error || `/${name} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u043E\u0448\u0438\u0431\u043A\u043E\u0439`;
|
|
3118
|
+
}
|
|
3119
|
+
} catch (e) {
|
|
3120
|
+
error = describe2(e);
|
|
3121
|
+
}
|
|
3122
|
+
if (this.#closing)
|
|
3123
|
+
return;
|
|
3124
|
+
if (reply)
|
|
3125
|
+
this.#record_({ kind: "assistant.text", text: reply, blockId: `cmd:${randomUUID2()}` });
|
|
3126
|
+
if (error)
|
|
3127
|
+
this.#record_({ kind: "error", message: error });
|
|
3128
|
+
if (turnRunning || this.#pendingTurns > 0)
|
|
3129
|
+
return;
|
|
3130
|
+
this.#record_({
|
|
3131
|
+
kind: "result",
|
|
3132
|
+
subtype: error ? "error" : "success",
|
|
3133
|
+
isError: Boolean(error),
|
|
3134
|
+
durationMs: Date.now() - started,
|
|
3135
|
+
costUsd: null,
|
|
3136
|
+
usage: null,
|
|
3137
|
+
contextTokens: this.#contextTokens,
|
|
3138
|
+
text: reply
|
|
3139
|
+
});
|
|
3140
|
+
this.#lastTurn = error ? "failed" : "completed";
|
|
3141
|
+
if (this.#state === "running")
|
|
3142
|
+
this.#setState("idle");
|
|
3143
|
+
else
|
|
3144
|
+
this.emit("summary", this.summary());
|
|
3145
|
+
}
|
|
3146
|
+
async #modelCommand(arg) {
|
|
3147
|
+
const models = this.#capabilities?.models ?? [];
|
|
3148
|
+
const label = (id2) => id2 ? models.find((m) => m.value === id2)?.displayName ?? id2 : "\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E agy";
|
|
3149
|
+
if (!arg) {
|
|
3150
|
+
const current = this.#record.model ?? this.#activeModel;
|
|
3151
|
+
return { reply: `\u041C\u043E\u0434\u0435\u043B\u044C: **${label(current)}**${current ? ` (\`${current}\`)` : ""}`, error: null };
|
|
3152
|
+
}
|
|
3153
|
+
const wanted = arg.toLowerCase();
|
|
3154
|
+
const match = models.find((m) => m.value.toLowerCase() === wanted) ?? models.find((m) => m.displayName.toLowerCase() === wanted);
|
|
3155
|
+
if (models.length > 0 && !match) {
|
|
3156
|
+
return {
|
|
3157
|
+
reply: null,
|
|
3158
|
+
error: `\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438 \xAB${arg}\xBB. \u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B: ${models.map((m) => m.value).join(", ")}`
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
const id = match?.value ?? arg;
|
|
3162
|
+
await this.setModel(id);
|
|
3163
|
+
return { reply: `\u041C\u043E\u0434\u0435\u043B\u044C \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0430: **${label(id)}** (\`${id}\`)`, error: null };
|
|
3164
|
+
}
|
|
3165
|
+
async #effortCommand(arg) {
|
|
3166
|
+
const levels = ["low", "medium", "high", "max"];
|
|
3167
|
+
if (!arg) {
|
|
3168
|
+
return { reply: `\u0423\u0441\u0438\u043B\u0438\u0435: **${this.#record.effort ?? "\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u043C\u043E\u0434\u0435\u043B\u0438"}**`, error: null };
|
|
3169
|
+
}
|
|
3170
|
+
const level = arg.toLowerCase();
|
|
3171
|
+
if (!levels.includes(level)) {
|
|
3172
|
+
return { reply: null, error: `\u0423\u0440\u043E\u0432\u0435\u043D\u044C \u0443\u0441\u0438\u043B\u0438\u044F \u2014 \u043E\u0434\u0438\u043D \u0438\u0437: ${levels.join(", ")}` };
|
|
3173
|
+
}
|
|
3174
|
+
await this.setEffort(level);
|
|
3175
|
+
return { reply: `\u0423\u0441\u0438\u043B\u0438\u0435: **${level}**`, error: null };
|
|
3176
|
+
}
|
|
3177
|
+
/** Картинки — файлами во временном каталоге, а в текст — их пути. */
|
|
3178
|
+
async #composePrompt(text, images) {
|
|
3179
|
+
if (!images || images.length === 0)
|
|
3180
|
+
return text;
|
|
3181
|
+
const dir = path7.join(tmpdir(), "ccv-antigravity", this.id);
|
|
3182
|
+
await mkdir2(dir, { recursive: true });
|
|
3183
|
+
const paths = [];
|
|
3184
|
+
for (const image of images) {
|
|
3185
|
+
const ext = image.mediaType.split("/")[1] === "jpeg" ? "jpg" : image.mediaType.split("/")[1];
|
|
3186
|
+
const file = path7.join(dir, `${randomUUID2()}.${ext}`);
|
|
3187
|
+
await writeFile2(file, Buffer.from(image.data, "base64"));
|
|
3188
|
+
paths.push(file);
|
|
3189
|
+
}
|
|
3190
|
+
return `${text}
|
|
3191
|
+
|
|
3192
|
+
Attached image${paths.length > 1 ? "s" : ""} (view them with your file viewing tool):
|
|
3193
|
+
` + paths.map((p) => `- ${p}`).join("\n");
|
|
3194
|
+
}
|
|
3195
|
+
async editPrompt(_userIndex, _text, _images) {
|
|
3196
|
+
throw new Error("\u0412 \u0441\u0435\u0441\u0441\u0438\u0438 Antigravity \u043F\u0440\u0430\u0432\u043A\u0430 \u043F\u0440\u043E\u0448\u043B\u044B\u0445 \u0440\u0435\u043F\u043B\u0438\u043A \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F");
|
|
3197
|
+
}
|
|
3198
|
+
respondPermission(_requestId, _decision, _reason, _answers, _annotations, _plan) {
|
|
3199
|
+
}
|
|
3200
|
+
async interrupt() {
|
|
3201
|
+
if (this.#pendingTurns === 0)
|
|
3202
|
+
return;
|
|
3203
|
+
this.#interrupting = true;
|
|
3204
|
+
this.#kill();
|
|
3205
|
+
this.#closeOpenSteps();
|
|
3206
|
+
this.#pendingTurns = 0;
|
|
3207
|
+
this.#lastTurn = "interrupted";
|
|
3208
|
+
this.#record_({
|
|
3209
|
+
kind: "result",
|
|
3210
|
+
subtype: "interrupted",
|
|
3211
|
+
isError: false,
|
|
3212
|
+
durationMs: 0,
|
|
3213
|
+
costUsd: null,
|
|
3214
|
+
usage: null,
|
|
3215
|
+
contextTokens: this.#contextTokens,
|
|
3216
|
+
text: null
|
|
3217
|
+
});
|
|
3218
|
+
this.#interrupting = false;
|
|
3219
|
+
this.#spawn();
|
|
3220
|
+
if (this.#state !== "error")
|
|
3221
|
+
this.#setState("idle");
|
|
3222
|
+
}
|
|
3223
|
+
async setPermissionMode(mode) {
|
|
3224
|
+
if (this.#record.permissionMode === mode)
|
|
3225
|
+
return;
|
|
3226
|
+
this.#record.permissionMode = mode;
|
|
3227
|
+
this.#settingsChanged();
|
|
3228
|
+
}
|
|
3229
|
+
async setModel(model) {
|
|
3230
|
+
if (this.#record.model === model)
|
|
3231
|
+
return;
|
|
3232
|
+
this.#record.model = model;
|
|
3233
|
+
this.#settingsChanged();
|
|
3234
|
+
}
|
|
3235
|
+
async setEffort(effort) {
|
|
3236
|
+
if (this.#record.effort === effort)
|
|
3237
|
+
return;
|
|
3238
|
+
this.#record.effort = effort;
|
|
3239
|
+
this.#settingsChanged();
|
|
3240
|
+
}
|
|
3241
|
+
/** Флаги запуска поменялись: новый процесс — сейчас или после хода. */
|
|
3242
|
+
#settingsChanged() {
|
|
3243
|
+
if (this.#capabilities) {
|
|
3244
|
+
this.#capabilities = this.#baseCapabilities();
|
|
3245
|
+
this.emit("capabilities");
|
|
3246
|
+
}
|
|
3247
|
+
this.emit("summary", this.summary());
|
|
3248
|
+
void this.#store.save(this.#record);
|
|
3249
|
+
if (!this.#executable || this.#state === "closed")
|
|
3250
|
+
return;
|
|
3251
|
+
if (this.#pendingTurns > 0)
|
|
3252
|
+
this.#restartPending = true;
|
|
3253
|
+
else
|
|
3254
|
+
this.#restart();
|
|
3255
|
+
}
|
|
3256
|
+
// Claude-only управление: у agy этого нет — молча ничего не делаем.
|
|
3257
|
+
async setUltracode(_enabled) {
|
|
3258
|
+
}
|
|
3259
|
+
async setThinking(_enabled) {
|
|
3260
|
+
}
|
|
3261
|
+
async setSwitchModelsOnFlag(_enabled) {
|
|
3262
|
+
}
|
|
3263
|
+
/** Квота `agy` (`/usage`) — общая на аккаунт, кэш один на хост. */
|
|
3264
|
+
async refreshUsage(force = false) {
|
|
3265
|
+
if (!this.#executable)
|
|
3266
|
+
return;
|
|
3267
|
+
const usage = await agyUsage(this.#executable, force);
|
|
3268
|
+
if (!usage)
|
|
3269
|
+
return;
|
|
3270
|
+
this.#usage = usage;
|
|
3271
|
+
this.emit("usage", usage);
|
|
3272
|
+
}
|
|
3273
|
+
async refreshContext(_force = false) {
|
|
3274
|
+
this.#emitContext();
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Снимок окна контекста: занятое — из `usage` последнего шага модели,
|
|
3278
|
+
* всего — по модели из таблицы. Чего-то из двух нет — молчим: полоса
|
|
3279
|
+
* останется со своим умолчанием и числом из `result`.
|
|
3280
|
+
*/
|
|
3281
|
+
#emitContext() {
|
|
3282
|
+
const maxTokens = agyContextWindow(this.#record.model ?? this.#activeModel);
|
|
3283
|
+
if (this.#contextTokens === null || maxTokens === null)
|
|
3284
|
+
return;
|
|
3285
|
+
this.#context = { tokens: this.#contextTokens, maxTokens, fetchedAt: Date.now() };
|
|
3286
|
+
this.emit("context", this.#context);
|
|
3287
|
+
}
|
|
3288
|
+
async reloadPlugins() {
|
|
3289
|
+
}
|
|
3290
|
+
async mcpServerStatus() {
|
|
3291
|
+
return [];
|
|
3292
|
+
}
|
|
3293
|
+
async addMcpServer(_name, _config) {
|
|
3294
|
+
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`)");
|
|
3295
|
+
}
|
|
3296
|
+
async removeMcpServer(_name) {
|
|
3297
|
+
return false;
|
|
3298
|
+
}
|
|
3299
|
+
setTitle(title) {
|
|
3300
|
+
this.#record.title = title;
|
|
3301
|
+
this.#titleIsDefault = false;
|
|
3302
|
+
void this.#store.save(this.#record);
|
|
3303
|
+
this.emit("summary", this.summary());
|
|
3304
|
+
}
|
|
3305
|
+
// -------------------------------------------------------------------------
|
|
3306
|
+
// Поток agy
|
|
3307
|
+
// -------------------------------------------------------------------------
|
|
3308
|
+
#handleLine(line) {
|
|
3309
|
+
const trimmed = line.trim();
|
|
3310
|
+
if (!trimmed.startsWith("{"))
|
|
3311
|
+
return;
|
|
3312
|
+
let message;
|
|
3313
|
+
try {
|
|
3314
|
+
message = JSON.parse(trimmed);
|
|
3315
|
+
} catch {
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
switch (message.event) {
|
|
3319
|
+
case "init": {
|
|
3320
|
+
const conversationId = typeof message.conversation_id === "string" ? message.conversation_id : null;
|
|
3321
|
+
if (conversationId && conversationId !== this.#record.conversationId) {
|
|
3322
|
+
this.#record.conversationId = conversationId;
|
|
3323
|
+
void this.#store.save(this.#record);
|
|
3324
|
+
}
|
|
3325
|
+
const init = message.init;
|
|
3326
|
+
if (typeof init?.model === "string" && init.model && init.model !== this.#activeModel) {
|
|
3327
|
+
this.#activeModel = init.model;
|
|
3328
|
+
if (this.#capabilities) {
|
|
3329
|
+
this.#capabilities = this.#baseCapabilities();
|
|
3330
|
+
this.emit("capabilities");
|
|
3331
|
+
}
|
|
3332
|
+
this.#emitContext();
|
|
3333
|
+
}
|
|
3334
|
+
if (this.#capabilities && Array.isArray(init?.tools)) {
|
|
3335
|
+
this.#capabilities = {
|
|
3336
|
+
...this.#capabilities,
|
|
3337
|
+
tools: init.tools.filter((t) => typeof t === "string")
|
|
3338
|
+
};
|
|
3339
|
+
this.emit("capabilities");
|
|
3340
|
+
}
|
|
3341
|
+
if (this.#state === "starting")
|
|
3342
|
+
this.#setState("idle");
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
case "step_update":
|
|
3346
|
+
this.#handleStep(message.step_update);
|
|
3347
|
+
return;
|
|
3348
|
+
case "result":
|
|
3349
|
+
this.#handleResult(message.result);
|
|
3350
|
+
return;
|
|
3351
|
+
default:
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
#handleStep(step2) {
|
|
3356
|
+
if (!step2 || typeof step2.step_index !== "number")
|
|
3357
|
+
return;
|
|
3358
|
+
const index = step2.step_index;
|
|
3359
|
+
const blockId = `${this.#record.conversationId ?? this.id}:${index}`;
|
|
3360
|
+
if (step2.step_type === "user_input")
|
|
3361
|
+
return;
|
|
3362
|
+
if (step2.step_type === "tool" || step2.tool_info || step2.tool_name) {
|
|
3363
|
+
const name = step2.tool_name ?? step2.tool_info?.name ?? "tool";
|
|
3364
|
+
if (!this.#tools.has(index)) {
|
|
3365
|
+
this.#tools.add(index);
|
|
3366
|
+
this.#record_({
|
|
3367
|
+
kind: "tool.use",
|
|
3368
|
+
toolUseId: blockId,
|
|
3369
|
+
name,
|
|
3370
|
+
input: toolInput(step2.tool_info?.parameters)
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
if (step2.state === "DONE" || step2.state === "ERROR") {
|
|
3374
|
+
const error = step2.tool_info?.error?.message;
|
|
3375
|
+
this.#record_({
|
|
3376
|
+
kind: "tool.result",
|
|
3377
|
+
toolUseId: blockId,
|
|
3378
|
+
content: error ?? step2.tool_info?.output ?? "",
|
|
3379
|
+
isError: step2.state === "ERROR" || Boolean(error)
|
|
3380
|
+
});
|
|
3381
|
+
this.#tools.delete(index);
|
|
3382
|
+
}
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
if (step2.thinking_delta) {
|
|
3386
|
+
this.#thinking.set(index, (this.#thinking.get(index) ?? "") + step2.thinking_delta);
|
|
3387
|
+
this.#record_({ kind: "thinking.delta", text: step2.thinking_delta, blockId: `${blockId}:t` });
|
|
3388
|
+
}
|
|
3389
|
+
if (step2.text_delta) {
|
|
3390
|
+
this.#texts.set(index, (this.#texts.get(index) ?? "") + step2.text_delta);
|
|
3391
|
+
this.#record_({ kind: "assistant.delta", text: step2.text_delta, blockId });
|
|
3392
|
+
}
|
|
3393
|
+
if (step2.usage && typeof step2.usage.input_tokens === "number") {
|
|
3394
|
+
this.#contextTokens = step2.usage.input_tokens + (step2.usage.output_tokens ?? 0);
|
|
3395
|
+
}
|
|
3396
|
+
if (step2.state === "DONE" || step2.state === "ERROR") {
|
|
3397
|
+
const thinking = this.#thinking.get(index);
|
|
3398
|
+
if (thinking) {
|
|
3399
|
+
this.#record_({ kind: "assistant.thinking", text: thinking, blockId: `${blockId}:t` });
|
|
3400
|
+
this.#thinking.delete(index);
|
|
3401
|
+
}
|
|
3402
|
+
const text = this.#texts.get(index);
|
|
3403
|
+
if (text) {
|
|
3404
|
+
this.#record_({ kind: "assistant.text", text: text.replace(/\n+$/, ""), blockId });
|
|
3405
|
+
this.#texts.delete(index);
|
|
3406
|
+
}
|
|
3407
|
+
if (step2.state === "ERROR") {
|
|
3408
|
+
const message = typeof step2.error === "string" ? step2.error : step2.error?.message;
|
|
3409
|
+
this.#record_({ kind: "error", message: message || `\u0428\u0430\u0433 ${step2.step_type ?? ""} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u043E\u0448\u0438\u0431\u043A\u043E\u0439` });
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
#handleResult(result) {
|
|
3414
|
+
if (!result || this.#interrupting)
|
|
3415
|
+
return;
|
|
3416
|
+
this.#closeOpenSteps();
|
|
3417
|
+
const ok = result.status === "SUCCESS";
|
|
3418
|
+
const usage = result.usage ? {
|
|
3419
|
+
inputTokens: result.usage.input_tokens ?? 0,
|
|
3420
|
+
outputTokens: (result.usage.output_tokens ?? 0) + (result.usage.thinking_tokens ?? 0),
|
|
3421
|
+
cacheReadTokens: result.usage.cache_read_tokens ?? 0,
|
|
3422
|
+
cacheCreationTokens: 0
|
|
3423
|
+
} : null;
|
|
3424
|
+
const denied = (result.denied_actions ?? []).map((a) => a.display_name ?? a.action).filter((a) => Boolean(a));
|
|
3425
|
+
if (denied.length > 0) {
|
|
3426
|
+
this.#record_({
|
|
3427
|
+
kind: "error",
|
|
3428
|
+
message: `Antigravity \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u0431\u0435\u0437 \u0432\u043E\u043F\u0440\u043E\u0441\u0430: ${[...new Set(denied)].join(", ")}. \u0412 headless-\u0440\u0435\u0436\u0438\u043C\u0435 agy \u043D\u0435 \u0443\u043C\u0435\u0435\u0442 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044C \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435: \u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u0430\u0439\u043B\u043E\u0432 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 \u0440\u0435\u0436\u0438\u043C acceptEdits, \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u0438 \u0432\u0441\u0451 \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u043E\u0435 \u2014 bypassPermissions. \u041E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u043C\u043E\u0436\u043D\u043E \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u043F\u0440\u0430\u0432\u0438\u043B\u043E\u043C \u0432 permissions.allow \u0444\u0430\u0439\u043B\u0430 ~/.gemini/antigravity-cli/settings.json, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 command(npm test).`
|
|
3429
|
+
});
|
|
3430
|
+
}
|
|
3431
|
+
if (!ok && result.error)
|
|
3432
|
+
this.#record_({ kind: "error", message: result.error });
|
|
3433
|
+
this.#record_({
|
|
3434
|
+
kind: "result",
|
|
3435
|
+
subtype: ok ? "success" : "error",
|
|
3436
|
+
isError: !ok,
|
|
3437
|
+
durationMs: Math.round((result.duration_seconds ?? 0) * 1e3),
|
|
3438
|
+
costUsd: null,
|
|
3439
|
+
usage,
|
|
3440
|
+
contextTokens: this.#contextTokens,
|
|
3441
|
+
text: result.response ?? null
|
|
3442
|
+
});
|
|
3443
|
+
this.#emitContext();
|
|
3444
|
+
void this.refreshUsage(true);
|
|
3445
|
+
this.#lastTurn = ok ? "completed" : "failed";
|
|
3446
|
+
this.#pendingTurns = Math.max(0, this.#pendingTurns - 1);
|
|
3447
|
+
if (this.#pendingTurns > 0)
|
|
3448
|
+
return;
|
|
3449
|
+
if (this.#restartPending)
|
|
3450
|
+
this.#restart();
|
|
3451
|
+
this.#setState("idle");
|
|
3452
|
+
}
|
|
3453
|
+
/** Незакрытые шаги оборванного хода: текст — в ленту, инструменты — с ошибкой. */
|
|
3454
|
+
#closeOpenSteps() {
|
|
3455
|
+
const prefix = this.#record.conversationId ?? this.id;
|
|
3456
|
+
for (const [index, text] of this.#texts) {
|
|
3457
|
+
if (text)
|
|
3458
|
+
this.#record_({ kind: "assistant.text", text, blockId: `${prefix}:${index}` });
|
|
3459
|
+
}
|
|
3460
|
+
for (const index of this.#tools) {
|
|
3461
|
+
this.#record_({
|
|
3462
|
+
kind: "tool.result",
|
|
3463
|
+
toolUseId: `${prefix}:${index}`,
|
|
3464
|
+
content: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E",
|
|
3465
|
+
isError: true
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
this.#texts.clear();
|
|
3469
|
+
this.#thinking.clear();
|
|
3470
|
+
this.#tools.clear();
|
|
3471
|
+
}
|
|
3472
|
+
// -------------------------------------------------------------------------
|
|
3473
|
+
#record_(event) {
|
|
3474
|
+
this.#updatedAt = Date.now();
|
|
3475
|
+
const entry = this.journal.append(event);
|
|
3476
|
+
this.emit("event", entry);
|
|
3477
|
+
if (TRANSIENT.has(event.kind))
|
|
3478
|
+
return;
|
|
3479
|
+
this.#unsaved.push({ ts: entry.ts, event });
|
|
3480
|
+
this.#scheduleFlush();
|
|
3481
|
+
}
|
|
3482
|
+
#scheduleFlush() {
|
|
3483
|
+
if (this.#flushTimer)
|
|
3484
|
+
return;
|
|
3485
|
+
this.#flushTimer = setTimeout(() => this.#flush(), 500);
|
|
3486
|
+
this.#flushTimer.unref?.();
|
|
3487
|
+
}
|
|
3488
|
+
#flush() {
|
|
3489
|
+
if (this.#flushTimer) {
|
|
3490
|
+
clearTimeout(this.#flushTimer);
|
|
3491
|
+
this.#flushTimer = null;
|
|
3492
|
+
}
|
|
3493
|
+
const batch = this.#unsaved;
|
|
3494
|
+
this.#unsaved = [];
|
|
3495
|
+
this.#record.updatedAt = this.#updatedAt;
|
|
3496
|
+
void this.#store.appendEvents(this.id, batch);
|
|
3497
|
+
void this.#store.save(this.#record);
|
|
3498
|
+
}
|
|
3499
|
+
#setState(state) {
|
|
3500
|
+
if (this.#state === state)
|
|
3501
|
+
return;
|
|
3502
|
+
this.#state = state;
|
|
3503
|
+
this.#record_({ kind: "state", state });
|
|
3504
|
+
if (state === "idle" || state === "error" || state === "closed")
|
|
3505
|
+
this.#flush();
|
|
3506
|
+
this.emit("summary", this.summary());
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
function toolInput(parameters) {
|
|
3510
|
+
const input = { ...parameters ?? {} };
|
|
3511
|
+
const pick = (...keys) => keys.map((k) => input[k]).find((v) => typeof v === "string");
|
|
3512
|
+
const file = pick("TargetFile", "AbsolutePath", "File", "FilePath", "Path");
|
|
3513
|
+
if (file && input.file_path === void 0)
|
|
3514
|
+
input.file_path = file;
|
|
3515
|
+
const command = pick("CommandLine", "Command");
|
|
3516
|
+
if (command && input.command === void 0)
|
|
3517
|
+
input.command = command;
|
|
3518
|
+
const pattern = pick("Query", "Pattern", "SearchPattern");
|
|
3519
|
+
if (pattern && input.pattern === void 0)
|
|
3520
|
+
input.pattern = pattern;
|
|
3521
|
+
const dir = pick("DirectoryPath", "SearchPath", "SearchDirectory");
|
|
3522
|
+
if (dir && input.path === void 0)
|
|
3523
|
+
input.path = dir;
|
|
3524
|
+
const url2 = pick("Url", "URL");
|
|
3525
|
+
if (url2 && input.url === void 0)
|
|
3526
|
+
input.url = url2;
|
|
3527
|
+
return input;
|
|
3528
|
+
}
|
|
3529
|
+
function formatUsage(usage) {
|
|
3530
|
+
const rows = (usage.buckets ?? []).map((b) => {
|
|
3531
|
+
const left = Math.round(100 - b.window.utilization);
|
|
3532
|
+
const reset = b.window.resetsAt ? new Date(b.window.resetsAt).toLocaleString("ru-RU") : "\u2014";
|
|
3533
|
+
return `| ${b.title || b.label} | ${left}% | ${reset} |`;
|
|
3534
|
+
});
|
|
3535
|
+
if (rows.length === 0)
|
|
3536
|
+
return "\u041A\u0432\u043E\u0442\u0430 Antigravity: \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435\u0442.";
|
|
3537
|
+
return ["**\u041A\u0432\u043E\u0442\u0430 Antigravity**", "", "| \u0413\u0440\u0443\u043F\u043F\u0430 | \u041E\u0441\u0442\u0430\u043B\u043E\u0441\u044C | \u041E\u0431\u043D\u0443\u043B\u0438\u0442\u0441\u044F |", "|---|---|---|", ...rows].join("\n");
|
|
3538
|
+
}
|
|
3539
|
+
function formatCommandReply(name, response) {
|
|
3540
|
+
const text = response.replace(/\s+$/, "");
|
|
3541
|
+
if (!text)
|
|
3542
|
+
return `/${name}: \u043F\u0443\u0441\u0442\u043E`;
|
|
3543
|
+
const lines = text.split(/\r?\n/);
|
|
3544
|
+
if (lines.every((l) => l.includes(" "))) {
|
|
3545
|
+
return lines.map((l) => {
|
|
3546
|
+
const [head, ...rest] = l.split(" ");
|
|
3547
|
+
return `- **${head}** \u2014 ${rest.join(" ")}`;
|
|
3548
|
+
}).join("\n");
|
|
2449
3549
|
}
|
|
3550
|
+
return text;
|
|
2450
3551
|
}
|
|
2451
|
-
function
|
|
2452
|
-
if (!
|
|
3552
|
+
function agyEffort(effort) {
|
|
3553
|
+
if (!effort)
|
|
2453
3554
|
return null;
|
|
2454
|
-
return
|
|
2455
|
-
// Доля окна и так приходит в 0–100, но полоса шире дорожки — это уже не
|
|
2456
|
-
// «почти всё израсходовано», а сломанная вёрстка.
|
|
2457
|
-
utilization: Math.min(100, Math.max(0, raw.utilization)),
|
|
2458
|
-
resetsAt: typeof raw.resets_at === "string" ? raw.resets_at : ""
|
|
2459
|
-
};
|
|
3555
|
+
return effort === "xhigh" ? "high" : effort;
|
|
2460
3556
|
}
|
|
2461
|
-
function
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
3557
|
+
function describe2(error) {
|
|
3558
|
+
return error instanceof Error ? error.message : String(error);
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3561
|
+
// ../host/dist/antigravity-store.js
|
|
3562
|
+
import { appendFile, mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
3563
|
+
import path8 from "node:path";
|
|
3564
|
+
var AntigravityStore = class {
|
|
3565
|
+
#dir;
|
|
3566
|
+
#file;
|
|
3567
|
+
#records = /* @__PURE__ */ new Map();
|
|
3568
|
+
#loaded = null;
|
|
3569
|
+
#writing = Promise.resolve();
|
|
3570
|
+
constructor(dataDir) {
|
|
3571
|
+
this.#dir = path8.join(dataDir, "antigravity");
|
|
3572
|
+
this.#file = path8.join(this.#dir, "sessions.json");
|
|
3573
|
+
}
|
|
3574
|
+
#load() {
|
|
3575
|
+
this.#loaded ??= (async () => {
|
|
3576
|
+
try {
|
|
3577
|
+
const raw = JSON.parse(await readFile4(this.#file, "utf8"));
|
|
3578
|
+
for (const record of Array.isArray(raw) ? raw : []) {
|
|
3579
|
+
if (record && typeof record.id === "string")
|
|
3580
|
+
this.#records.set(record.id, record);
|
|
3581
|
+
}
|
|
3582
|
+
} catch {
|
|
3583
|
+
}
|
|
3584
|
+
})();
|
|
3585
|
+
return this.#loaded;
|
|
3586
|
+
}
|
|
3587
|
+
async find(id) {
|
|
3588
|
+
await this.#load();
|
|
3589
|
+
return this.#records.get(id) ?? null;
|
|
3590
|
+
}
|
|
3591
|
+
async list(workspaceId) {
|
|
3592
|
+
await this.#load();
|
|
3593
|
+
return [...this.#records.values()].filter((r) => r.workspaceId === workspaceId);
|
|
3594
|
+
}
|
|
3595
|
+
async save(record) {
|
|
3596
|
+
await this.#load();
|
|
3597
|
+
this.#records.set(record.id, { ...record });
|
|
3598
|
+
await this.#persist();
|
|
3599
|
+
}
|
|
3600
|
+
async rename(id, title) {
|
|
3601
|
+
await this.#load();
|
|
3602
|
+
const record = this.#records.get(id);
|
|
3603
|
+
if (!record)
|
|
3604
|
+
return false;
|
|
3605
|
+
record.title = title;
|
|
3606
|
+
await this.#persist();
|
|
3607
|
+
return true;
|
|
3608
|
+
}
|
|
3609
|
+
async remove(id) {
|
|
3610
|
+
await this.#load();
|
|
3611
|
+
if (!this.#records.delete(id))
|
|
3612
|
+
return;
|
|
3613
|
+
await this.#persist();
|
|
3614
|
+
await rm2(this.#eventsFile(id), { force: true });
|
|
3615
|
+
}
|
|
3616
|
+
/** Дописать события в ленту сессии. Порядок записей сохраняется. */
|
|
3617
|
+
appendEvents(id, events) {
|
|
3618
|
+
if (events.length === 0)
|
|
3619
|
+
return this.#writing;
|
|
3620
|
+
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
3621
|
+
return this.#enqueue(async () => {
|
|
3622
|
+
await mkdir3(this.#dir, { recursive: true });
|
|
3623
|
+
await appendFile(this.#eventsFile(id), lines, "utf8");
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
/** Лента сессии как история: `seq` — позиция, от 1 по порядку. */
|
|
3627
|
+
async readEvents(id) {
|
|
3628
|
+
await this.#writing;
|
|
3629
|
+
let raw;
|
|
3630
|
+
try {
|
|
3631
|
+
raw = await readFile4(this.#eventsFile(id), "utf8");
|
|
3632
|
+
} catch {
|
|
3633
|
+
return [];
|
|
2471
3634
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
3635
|
+
const entries = [];
|
|
3636
|
+
for (const line of raw.split("\n")) {
|
|
3637
|
+
if (!line.trim())
|
|
3638
|
+
continue;
|
|
3639
|
+
try {
|
|
3640
|
+
const parsed = JSON.parse(line);
|
|
3641
|
+
entries.push({ seq: entries.length + 1, ts: parsed.ts, event: parsed.event });
|
|
3642
|
+
} catch {
|
|
3643
|
+
}
|
|
2474
3644
|
}
|
|
2475
|
-
return
|
|
2476
|
-
}
|
|
2477
|
-
|
|
3645
|
+
return entries;
|
|
3646
|
+
}
|
|
3647
|
+
#eventsFile(id) {
|
|
3648
|
+
return path8.join(this.#dir, `${id.replace(/[^\w-]/g, "_")}.jsonl`);
|
|
3649
|
+
}
|
|
3650
|
+
#persist() {
|
|
3651
|
+
const snapshot = JSON.stringify([...this.#records.values()], null, 2);
|
|
3652
|
+
return this.#enqueue(async () => {
|
|
3653
|
+
await mkdir3(this.#dir, { recursive: true });
|
|
3654
|
+
await writeFile3(this.#file, snapshot, "utf8");
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
/** Записи на диск — строго по очереди: две параллельные испортили бы файл. */
|
|
3658
|
+
#enqueue(task) {
|
|
3659
|
+
const next = this.#writing.then(task, task);
|
|
3660
|
+
this.#writing = next.catch((error) => {
|
|
3661
|
+
console.error("[antigravity-store]", error instanceof Error ? error.message : error);
|
|
3662
|
+
});
|
|
3663
|
+
return this.#writing;
|
|
3664
|
+
}
|
|
3665
|
+
};
|
|
2478
3666
|
|
|
2479
3667
|
// ../host/dist/git.js
|
|
2480
|
-
import { execFile as
|
|
2481
|
-
import { mkdtemp, readFile as
|
|
2482
|
-
import { tmpdir } from "node:os";
|
|
2483
|
-
import
|
|
3668
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3669
|
+
import { mkdtemp, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
3670
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3671
|
+
import path9 from "node:path";
|
|
2484
3672
|
var TIMEOUT_MS = 2e4;
|
|
2485
3673
|
var NET_TIMEOUT_MS = 5 * 6e4;
|
|
2486
3674
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
@@ -2526,7 +3714,7 @@ var GitRepo = class {
|
|
|
2526
3714
|
await this.#run(["rev-parse", "--is-inside-work-tree"]);
|
|
2527
3715
|
} catch (error) {
|
|
2528
3716
|
const runnable2 = await this.#run(["--version"]).then(() => true, () => false);
|
|
2529
|
-
return { ...blank, message:
|
|
3717
|
+
return { ...blank, message: describe3(error), noRepo: runnable2 };
|
|
2530
3718
|
}
|
|
2531
3719
|
const branchRaw = await this.#soft(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2532
3720
|
const detached = branchRaw === "HEAD" || branchRaw === "";
|
|
@@ -2796,12 +3984,12 @@ var GitRepo = class {
|
|
|
2796
3984
|
* важнее всего (см. `checkIgnored`).
|
|
2797
3985
|
*/
|
|
2798
3986
|
async #withoutRepo(args) {
|
|
2799
|
-
const dir = await mkdtemp(
|
|
3987
|
+
const dir = await mkdtemp(path9.join(tmpdir2(), "ccv-git-"));
|
|
2800
3988
|
try {
|
|
2801
3989
|
await this.#run(["init", "--bare", "--quiet", dir]);
|
|
2802
3990
|
return await this.#soft([`--git-dir=${dir}`, `--work-tree=${this.cwd}`, ...args]);
|
|
2803
3991
|
} finally {
|
|
2804
|
-
await
|
|
3992
|
+
await rm3(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2805
3993
|
}
|
|
2806
3994
|
}
|
|
2807
3995
|
/**
|
|
@@ -2842,14 +4030,14 @@ var GitRepo = class {
|
|
|
2842
4030
|
* исключил. Повторы отсеиваются, чтобы файл не рос от каждой публикации.
|
|
2843
4031
|
*/
|
|
2844
4032
|
async #appendGitignore(paths) {
|
|
2845
|
-
const file =
|
|
2846
|
-
const existing = await
|
|
4033
|
+
const file = path9.join(this.cwd, ".gitignore");
|
|
4034
|
+
const existing = await readFile5(file, "utf8").catch(() => "");
|
|
2847
4035
|
const known = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
2848
4036
|
const lines = paths.map(ignorePattern).filter((line) => !known.has(line));
|
|
2849
4037
|
if (lines.length === 0)
|
|
2850
4038
|
return;
|
|
2851
4039
|
const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
2852
|
-
await
|
|
4040
|
+
await writeFile4(file, `${existing}${prefix}${lines.join("\n")}
|
|
2853
4041
|
`, "utf8");
|
|
2854
4042
|
}
|
|
2855
4043
|
/** Команда, чей провал — обычное дело (нет upstream, нет коммитов, detached). */
|
|
@@ -2867,7 +4055,7 @@ var GitRepo = class {
|
|
|
2867
4055
|
#run(args, options2 = {}) {
|
|
2868
4056
|
const token = options2.token ?? "";
|
|
2869
4057
|
return new Promise((resolve, reject) => {
|
|
2870
|
-
const child =
|
|
4058
|
+
const child = execFile4(
|
|
2871
4059
|
"git",
|
|
2872
4060
|
// core.quotepath=false — иначе кириллические пути приезжают в виде
|
|
2873
4061
|
// «\320\277\321\200…» и в панели читаются как мусор.
|
|
@@ -2938,7 +4126,7 @@ function requirePath(value) {
|
|
|
2938
4126
|
function ignorePattern(relPath) {
|
|
2939
4127
|
return `/${relPath.replace(/[\\*?[\]]/g, (char) => `\\${char}`)}`;
|
|
2940
4128
|
}
|
|
2941
|
-
function
|
|
4129
|
+
function describe3(error) {
|
|
2942
4130
|
return error instanceof Error ? error.message : String(error);
|
|
2943
4131
|
}
|
|
2944
4132
|
function parseStatus(raw) {
|
|
@@ -3048,9 +4236,9 @@ function parsePatch(raw) {
|
|
|
3048
4236
|
body = "";
|
|
3049
4237
|
}
|
|
3050
4238
|
total += body.length;
|
|
3051
|
-
const
|
|
4239
|
+
const path25 = status === "deleted" ? oldPath : newPath || oldPath;
|
|
3052
4240
|
files.push({
|
|
3053
|
-
path:
|
|
4241
|
+
path: path25,
|
|
3054
4242
|
oldPath: status === "renamed" ? oldPath : "",
|
|
3055
4243
|
status,
|
|
3056
4244
|
added: count(body, "+"),
|
|
@@ -3062,10 +4250,10 @@ function parsePatch(raw) {
|
|
|
3062
4250
|
return { files, truncated };
|
|
3063
4251
|
}
|
|
3064
4252
|
function stripPrefix(value) {
|
|
3065
|
-
const
|
|
3066
|
-
if (
|
|
4253
|
+
const path25 = unquote(value.trim());
|
|
4254
|
+
if (path25 === "/dev/null")
|
|
3067
4255
|
return "";
|
|
3068
|
-
return
|
|
4256
|
+
return path25.replace(/^[ab]\//, "");
|
|
3069
4257
|
}
|
|
3070
4258
|
function count(body, mark) {
|
|
3071
4259
|
let n = 0;
|
|
@@ -3077,9 +4265,9 @@ function count(body, mark) {
|
|
|
3077
4265
|
}
|
|
3078
4266
|
|
|
3079
4267
|
// ../host/dist/github.js
|
|
3080
|
-
import { execFile as
|
|
3081
|
-
import { mkdir as
|
|
3082
|
-
import
|
|
4268
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
4269
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
|
|
4270
|
+
import path10 from "node:path";
|
|
3083
4271
|
var API = "https://api.github.com";
|
|
3084
4272
|
var API_VERSION = "2022-11-28";
|
|
3085
4273
|
var API_TIMEOUT_MS = 2e4;
|
|
@@ -3100,11 +4288,11 @@ var GitHubAccount = class {
|
|
|
3100
4288
|
*/
|
|
3101
4289
|
#ghInstalled = null;
|
|
3102
4290
|
constructor(dataDir) {
|
|
3103
|
-
this.#storePath =
|
|
4291
|
+
this.#storePath = path10.join(dataDir, "github.json");
|
|
3104
4292
|
}
|
|
3105
4293
|
async load() {
|
|
3106
4294
|
try {
|
|
3107
|
-
const raw = JSON.parse(await
|
|
4295
|
+
const raw = JSON.parse(await readFile6(this.#storePath, "utf8"));
|
|
3108
4296
|
const token = raw.token;
|
|
3109
4297
|
this.#token = typeof token === "string" ? token.trim() : "";
|
|
3110
4298
|
} catch {
|
|
@@ -3114,8 +4302,8 @@ var GitHubAccount = class {
|
|
|
3114
4302
|
/** Сохранить токен или стереть его пустой строкой. */
|
|
3115
4303
|
async setToken(token) {
|
|
3116
4304
|
this.#token = token.trim();
|
|
3117
|
-
await
|
|
3118
|
-
await
|
|
4305
|
+
await mkdir4(path10.dirname(this.#storePath), { recursive: true });
|
|
4306
|
+
await writeFile5(this.#storePath, JSON.stringify({ token: this.#token }, null, 2), {
|
|
3119
4307
|
encoding: "utf8",
|
|
3120
4308
|
mode: 384
|
|
3121
4309
|
});
|
|
@@ -3142,7 +4330,7 @@ var GitHubAccount = class {
|
|
|
3142
4330
|
const credential = await this.credential();
|
|
3143
4331
|
return { ...base, connected: true, source: credential.source, login: credential.login };
|
|
3144
4332
|
} catch (error) {
|
|
3145
|
-
return { ...base, message:
|
|
4333
|
+
return { ...base, message: describe4(error) };
|
|
3146
4334
|
}
|
|
3147
4335
|
}
|
|
3148
4336
|
/**
|
|
@@ -3240,7 +4428,7 @@ var GitHubAccount = class {
|
|
|
3240
4428
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
3241
4429
|
});
|
|
3242
4430
|
} catch (error) {
|
|
3243
|
-
throw new Error(`GitHub \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${
|
|
4431
|
+
throw new Error(`GitHub \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${describe4(error)}`);
|
|
3244
4432
|
}
|
|
3245
4433
|
const text = await response.text();
|
|
3246
4434
|
if (!response.ok)
|
|
@@ -3266,7 +4454,7 @@ var GitHubAccount = class {
|
|
|
3266
4454
|
};
|
|
3267
4455
|
function gh(args) {
|
|
3268
4456
|
return new Promise((resolve, reject) => {
|
|
3269
|
-
|
|
4457
|
+
execFile5("gh", args, { timeout: GH_TIMEOUT_MS, windowsHide: true, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
3270
4458
|
if (error)
|
|
3271
4459
|
return reject(new Error(stderr.trim() || error.message));
|
|
3272
4460
|
resolve(stdout);
|
|
@@ -3302,7 +4490,7 @@ function apiError(status, body) {
|
|
|
3302
4490
|
function str(value) {
|
|
3303
4491
|
return typeof value === "string" ? value : "";
|
|
3304
4492
|
}
|
|
3305
|
-
function
|
|
4493
|
+
function describe4(error) {
|
|
3306
4494
|
return error instanceof Error ? error.message : String(error);
|
|
3307
4495
|
}
|
|
3308
4496
|
|
|
@@ -3310,7 +4498,7 @@ function describe3(error) {
|
|
|
3310
4498
|
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
3311
4499
|
import { readdir as readdir2 } from "node:fs/promises";
|
|
3312
4500
|
import os2 from "node:os";
|
|
3313
|
-
import
|
|
4501
|
+
import path11 from "node:path";
|
|
3314
4502
|
import { listSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
3315
4503
|
var CACHE_TTL = 6e4;
|
|
3316
4504
|
var FIND_LIMIT = 50;
|
|
@@ -3349,7 +4537,7 @@ var ProjectFinder = class {
|
|
|
3349
4537
|
if (needles.length === 0)
|
|
3350
4538
|
return { candidates: [], truncated: false };
|
|
3351
4539
|
const open = new Map(this.registry.list().map((workspace) => [workspace.path.toLowerCase(), workspace]));
|
|
3352
|
-
const byName = this.#dirs.filter((dir) => matches(
|
|
4540
|
+
const byName = this.#dirs.filter((dir) => matches(path11.basename(dir.path), needles));
|
|
3353
4541
|
const matched = byName.length > 0 ? byName : this.#dirs.filter((dir) => matches(dir.path, needles));
|
|
3354
4542
|
const sorted = [...matched].sort((a, b) => {
|
|
3355
4543
|
const aOpen = open.has(a.path.toLowerCase());
|
|
@@ -3360,7 +4548,7 @@ var ProjectFinder = class {
|
|
|
3360
4548
|
return a.sessions > 0 ? -1 : 1;
|
|
3361
4549
|
if (a.lastUsedAt !== b.lastUsedAt)
|
|
3362
4550
|
return b.lastUsedAt - a.lastUsedAt;
|
|
3363
|
-
return
|
|
4551
|
+
return path11.basename(a.path).localeCompare(path11.basename(b.path), void 0, {
|
|
3364
4552
|
numeric: true,
|
|
3365
4553
|
sensitivity: "base"
|
|
3366
4554
|
});
|
|
@@ -3368,7 +4556,7 @@ var ProjectFinder = class {
|
|
|
3368
4556
|
return {
|
|
3369
4557
|
candidates: sorted.slice(0, FIND_LIMIT).map((dir) => ({
|
|
3370
4558
|
path: dir.path,
|
|
3371
|
-
name:
|
|
4559
|
+
name: path11.basename(dir.path) || dir.path,
|
|
3372
4560
|
sessions: dir.sessions,
|
|
3373
4561
|
lastUsedAt: dir.lastUsedAt || null,
|
|
3374
4562
|
open: open.has(dir.path.toLowerCase()),
|
|
@@ -3392,7 +4580,7 @@ var ProjectFinder = class {
|
|
|
3392
4580
|
const candidates = [];
|
|
3393
4581
|
for (const entry of named) {
|
|
3394
4582
|
const absolute = entry.path.trim();
|
|
3395
|
-
if (!absolute || !
|
|
4583
|
+
if (!absolute || !path11.isAbsolute(absolute))
|
|
3396
4584
|
continue;
|
|
3397
4585
|
const key = absolute.toLowerCase();
|
|
3398
4586
|
if (seen.has(key))
|
|
@@ -3403,7 +4591,7 @@ var ProjectFinder = class {
|
|
|
3403
4591
|
const record = known.get(key);
|
|
3404
4592
|
candidates.push({
|
|
3405
4593
|
path: record?.path ?? absolute,
|
|
3406
|
-
name:
|
|
4594
|
+
name: path11.basename(absolute) || absolute,
|
|
3407
4595
|
sessions: record?.sessions ?? 0,
|
|
3408
4596
|
lastUsedAt: record?.lastUsedAt || null,
|
|
3409
4597
|
open: open.has(key),
|
|
@@ -3460,7 +4648,7 @@ var ProjectFinder = class {
|
|
|
3460
4648
|
if (!dirent.isDirectory() || SKIP_NAMES.has(dirent.name) || dirent.name.startsWith(".")) {
|
|
3461
4649
|
continue;
|
|
3462
4650
|
}
|
|
3463
|
-
const full =
|
|
4651
|
+
const full = path11.join(parent, dirent.name);
|
|
3464
4652
|
const key = full.toLowerCase();
|
|
3465
4653
|
if (dirs.has(key))
|
|
3466
4654
|
continue;
|
|
@@ -3481,7 +4669,7 @@ var ProjectFinder = class {
|
|
|
3481
4669
|
#parents(dirs) {
|
|
3482
4670
|
const counted = /* @__PURE__ */ new Map();
|
|
3483
4671
|
for (const dir of dirs) {
|
|
3484
|
-
const parent =
|
|
4672
|
+
const parent = path11.dirname(dir.path);
|
|
3485
4673
|
if (parent === dir.path || !usable(parent))
|
|
3486
4674
|
continue;
|
|
3487
4675
|
const key = parent.toLowerCase();
|
|
@@ -3593,18 +4781,18 @@ function fromText(text) {
|
|
|
3593
4781
|
|
|
3594
4782
|
// ../host/dist/workspaces.js
|
|
3595
4783
|
import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
|
|
3596
|
-
import { mkdir as
|
|
3597
|
-
import
|
|
4784
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
|
|
4785
|
+
import path12 from "node:path";
|
|
3598
4786
|
import { listSessions as listSessions2 } from "@anthropic-ai/claude-agent-sdk";
|
|
3599
4787
|
var WorkspaceRegistry = class {
|
|
3600
4788
|
#entries = [];
|
|
3601
4789
|
#storePath;
|
|
3602
4790
|
constructor(dataDir) {
|
|
3603
|
-
this.#storePath =
|
|
4791
|
+
this.#storePath = path12.join(dataDir, "workspaces.json");
|
|
3604
4792
|
}
|
|
3605
4793
|
async load() {
|
|
3606
4794
|
try {
|
|
3607
|
-
const raw = await
|
|
4795
|
+
const raw = await readFile7(this.#storePath, "utf8");
|
|
3608
4796
|
const parsed = JSON.parse(raw);
|
|
3609
4797
|
if (!Array.isArray(parsed)) {
|
|
3610
4798
|
this.#entries = [];
|
|
@@ -3622,8 +4810,8 @@ var WorkspaceRegistry = class {
|
|
|
3622
4810
|
}
|
|
3623
4811
|
}
|
|
3624
4812
|
async #persist() {
|
|
3625
|
-
await
|
|
3626
|
-
await
|
|
4813
|
+
await mkdir5(path12.dirname(this.#storePath), { recursive: true });
|
|
4814
|
+
await writeFile6(this.#storePath, JSON.stringify(this.#entries, null, 2), "utf8");
|
|
3627
4815
|
}
|
|
3628
4816
|
/** Активные проекты. */
|
|
3629
4817
|
list() {
|
|
@@ -3638,7 +4826,7 @@ var WorkspaceRegistry = class {
|
|
|
3638
4826
|
return entry ? toInfo(entry) : void 0;
|
|
3639
4827
|
}
|
|
3640
4828
|
async open(rawPath) {
|
|
3641
|
-
const absolute =
|
|
4829
|
+
const absolute = path12.resolve(rawPath);
|
|
3642
4830
|
if (!existsSync3(absolute)) {
|
|
3643
4831
|
throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${absolute}`);
|
|
3644
4832
|
}
|
|
@@ -3705,24 +4893,24 @@ var WorkspaceRegistry = class {
|
|
|
3705
4893
|
async create(parentRaw, nameRaw, init) {
|
|
3706
4894
|
const name = nameRaw.trim();
|
|
3707
4895
|
requireName(name);
|
|
3708
|
-
const parent =
|
|
4896
|
+
const parent = path12.resolve(parentRaw.trim());
|
|
3709
4897
|
if (!existsSync3(parent)) {
|
|
3710
4898
|
throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${parent}`);
|
|
3711
4899
|
}
|
|
3712
4900
|
if (!statSync2(parent).isDirectory()) {
|
|
3713
4901
|
throw new Error(`\u042D\u0442\u043E \u043D\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433: ${parent}`);
|
|
3714
4902
|
}
|
|
3715
|
-
const target =
|
|
4903
|
+
const target = path12.join(parent, name);
|
|
3716
4904
|
if (existsSync3(target)) {
|
|
3717
4905
|
throw new Error(`\u0423\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442: ${target} \u2014 \u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0435\u0433\u043E \u043A\u0430\u043A \u043F\u0440\u043E\u0435\u043A\u0442`);
|
|
3718
4906
|
}
|
|
3719
|
-
await
|
|
4907
|
+
await mkdir5(target);
|
|
3720
4908
|
const warnings = [];
|
|
3721
4909
|
if (init.claudeMd) {
|
|
3722
|
-
await step(warnings, "CLAUDE.md", () =>
|
|
4910
|
+
await step(warnings, "CLAUDE.md", () => writeFile6(path12.join(target, "CLAUDE.md"), claudeMdStub(name), "utf8"));
|
|
3723
4911
|
}
|
|
3724
4912
|
if (init.readme) {
|
|
3725
|
-
await step(warnings, "README.md", () =>
|
|
4913
|
+
await step(warnings, "README.md", () => writeFile6(path12.join(target, "README.md"), readmeStub(name), "utf8"));
|
|
3726
4914
|
}
|
|
3727
4915
|
if (init.git) {
|
|
3728
4916
|
await step(warnings, "git init", () => new GitRepo(target).init());
|
|
@@ -3794,8 +4982,8 @@ function toInfo(entry) {
|
|
|
3794
4982
|
return {
|
|
3795
4983
|
id: workspaceIdFromPath(absolute),
|
|
3796
4984
|
path: absolute,
|
|
3797
|
-
name:
|
|
3798
|
-
hasClaudeMd: existsSync3(
|
|
4985
|
+
name: path12.basename(absolute) || absolute,
|
|
4986
|
+
hasClaudeMd: existsSync3(path12.join(absolute, "CLAUDE.md")),
|
|
3799
4987
|
...archivedAt ? { archivedAt } : {}
|
|
3800
4988
|
};
|
|
3801
4989
|
}
|
|
@@ -3804,18 +4992,18 @@ function isEntry(value) {
|
|
|
3804
4992
|
}
|
|
3805
4993
|
|
|
3806
4994
|
// ../host/dist/archive.js
|
|
3807
|
-
import { mkdir as
|
|
3808
|
-
import
|
|
4995
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
|
|
4996
|
+
import path13 from "node:path";
|
|
3809
4997
|
var SessionArchive = class {
|
|
3810
4998
|
#archived = /* @__PURE__ */ new Map();
|
|
3811
4999
|
#restored = /* @__PURE__ */ new Map();
|
|
3812
5000
|
#storePath;
|
|
3813
5001
|
constructor(dataDir) {
|
|
3814
|
-
this.#storePath =
|
|
5002
|
+
this.#storePath = path13.join(dataDir, "session-archive.json");
|
|
3815
5003
|
}
|
|
3816
5004
|
async load() {
|
|
3817
5005
|
try {
|
|
3818
|
-
const raw = await
|
|
5006
|
+
const raw = await readFile8(this.#storePath, "utf8");
|
|
3819
5007
|
const parsed = JSON.parse(raw);
|
|
3820
5008
|
this.#archived = new Map(Object.entries(parsed.archived ?? {}).filter(([, r]) => isRecord(r)));
|
|
3821
5009
|
this.#restored = new Map(Object.entries(parsed.restored ?? {}).filter((entry) => typeof entry[1] === "number"));
|
|
@@ -3904,8 +5092,8 @@ var SessionArchive = class {
|
|
|
3904
5092
|
archived: Object.fromEntries(this.#archived),
|
|
3905
5093
|
restored: Object.fromEntries(this.#restored)
|
|
3906
5094
|
};
|
|
3907
|
-
await
|
|
3908
|
-
await
|
|
5095
|
+
await mkdir6(path13.dirname(this.#storePath), { recursive: true });
|
|
5096
|
+
await writeFile7(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
3909
5097
|
}
|
|
3910
5098
|
};
|
|
3911
5099
|
function isRecord(value) {
|
|
@@ -3913,17 +5101,17 @@ function isRecord(value) {
|
|
|
3913
5101
|
}
|
|
3914
5102
|
|
|
3915
5103
|
// ../host/dist/pins.js
|
|
3916
|
-
import { mkdir as
|
|
3917
|
-
import
|
|
5104
|
+
import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
|
|
5105
|
+
import path14 from "node:path";
|
|
3918
5106
|
var SessionPins = class {
|
|
3919
5107
|
#pinned = /* @__PURE__ */ new Map();
|
|
3920
5108
|
#storePath;
|
|
3921
5109
|
constructor(dataDir) {
|
|
3922
|
-
this.#storePath =
|
|
5110
|
+
this.#storePath = path14.join(dataDir, "session-pins.json");
|
|
3923
5111
|
}
|
|
3924
5112
|
async load() {
|
|
3925
5113
|
try {
|
|
3926
|
-
const raw = await
|
|
5114
|
+
const raw = await readFile9(this.#storePath, "utf8");
|
|
3927
5115
|
const parsed = JSON.parse(raw);
|
|
3928
5116
|
this.#pinned = new Map(Object.entries(parsed.pinned ?? {}).filter(([, r]) => isRecord2(r)));
|
|
3929
5117
|
} catch {
|
|
@@ -4003,8 +5191,8 @@ var SessionPins = class {
|
|
|
4003
5191
|
}
|
|
4004
5192
|
async #persist() {
|
|
4005
5193
|
const file = { pinned: Object.fromEntries(this.#pinned) };
|
|
4006
|
-
await
|
|
4007
|
-
await
|
|
5194
|
+
await mkdir7(path14.dirname(this.#storePath), { recursive: true });
|
|
5195
|
+
await writeFile8(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
4008
5196
|
}
|
|
4009
5197
|
};
|
|
4010
5198
|
function isRecord2(value) {
|
|
@@ -4012,17 +5200,17 @@ function isRecord2(value) {
|
|
|
4012
5200
|
}
|
|
4013
5201
|
|
|
4014
5202
|
// ../host/dist/order.js
|
|
4015
|
-
import { mkdir as
|
|
4016
|
-
import
|
|
5203
|
+
import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile9 } from "node:fs/promises";
|
|
5204
|
+
import path15 from "node:path";
|
|
4017
5205
|
var ListOrderStore = class {
|
|
4018
5206
|
#order = emptyOrder();
|
|
4019
5207
|
#storePath;
|
|
4020
5208
|
constructor(dataDir) {
|
|
4021
|
-
this.#storePath =
|
|
5209
|
+
this.#storePath = path15.join(dataDir, "list-order.json");
|
|
4022
5210
|
}
|
|
4023
5211
|
async load() {
|
|
4024
5212
|
try {
|
|
4025
|
-
const raw = await
|
|
5213
|
+
const raw = await readFile10(this.#storePath, "utf8");
|
|
4026
5214
|
this.#order = sanitize(JSON.parse(raw));
|
|
4027
5215
|
} catch {
|
|
4028
5216
|
this.#order = emptyOrder();
|
|
@@ -4052,8 +5240,8 @@ var ListOrderStore = class {
|
|
|
4052
5240
|
} else {
|
|
4053
5241
|
this.#order[scope] = clean ?? [];
|
|
4054
5242
|
}
|
|
4055
|
-
await
|
|
4056
|
-
await
|
|
5243
|
+
await mkdir8(path15.dirname(this.#storePath), { recursive: true });
|
|
5244
|
+
await writeFile9(this.#storePath, JSON.stringify(this.#order, null, 2), "utf8");
|
|
4057
5245
|
}
|
|
4058
5246
|
};
|
|
4059
5247
|
function emptyOrder() {
|
|
@@ -4082,9 +5270,9 @@ function sanitize(value) {
|
|
|
4082
5270
|
}
|
|
4083
5271
|
|
|
4084
5272
|
// ../host/dist/folders.js
|
|
4085
|
-
import { mkdir as
|
|
4086
|
-
import
|
|
4087
|
-
import { randomUUID as
|
|
5273
|
+
import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile10 } from "node:fs/promises";
|
|
5274
|
+
import path16 from "node:path";
|
|
5275
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4088
5276
|
var Folders = class {
|
|
4089
5277
|
#workspaceFolders = /* @__PURE__ */ new Map();
|
|
4090
5278
|
#sessionFolders = /* @__PURE__ */ new Map();
|
|
@@ -4094,11 +5282,11 @@ var Folders = class {
|
|
|
4094
5282
|
#pinnedAssignments = /* @__PURE__ */ new Map();
|
|
4095
5283
|
#storePath;
|
|
4096
5284
|
constructor(dataDir) {
|
|
4097
|
-
this.#storePath =
|
|
5285
|
+
this.#storePath = path16.join(dataDir, "folders.json");
|
|
4098
5286
|
}
|
|
4099
5287
|
async load() {
|
|
4100
5288
|
try {
|
|
4101
|
-
const raw = await
|
|
5289
|
+
const raw = await readFile11(this.#storePath, "utf8");
|
|
4102
5290
|
const parsed = JSON.parse(raw);
|
|
4103
5291
|
this.#workspaceFolders = new Map(Object.entries(parsed.workspaceFolders ?? {}).filter(([, r]) => isWorkspaceFolder(r)));
|
|
4104
5292
|
this.#sessionFolders = new Map(Object.entries(parsed.sessionFolders ?? {}).filter(([, r]) => isSessionFolder(r)));
|
|
@@ -4128,19 +5316,19 @@ var Folders = class {
|
|
|
4128
5316
|
};
|
|
4129
5317
|
}
|
|
4130
5318
|
async createWorkspaceFolder(name) {
|
|
4131
|
-
const id =
|
|
5319
|
+
const id = randomUUID3();
|
|
4132
5320
|
this.#workspaceFolders.set(id, { name });
|
|
4133
5321
|
await this.#persist();
|
|
4134
5322
|
return id;
|
|
4135
5323
|
}
|
|
4136
5324
|
async createSessionFolder(workspaceId, name) {
|
|
4137
|
-
const id =
|
|
5325
|
+
const id = randomUUID3();
|
|
4138
5326
|
this.#sessionFolders.set(id, { name, workspaceId });
|
|
4139
5327
|
await this.#persist();
|
|
4140
5328
|
return id;
|
|
4141
5329
|
}
|
|
4142
5330
|
async createPinnedFolder(name) {
|
|
4143
|
-
const id =
|
|
5331
|
+
const id = randomUUID3();
|
|
4144
5332
|
this.#pinnedFolders.set(id, { name });
|
|
4145
5333
|
await this.#persist();
|
|
4146
5334
|
return id;
|
|
@@ -4281,8 +5469,8 @@ var Folders = class {
|
|
|
4281
5469
|
sessionAssignments: Object.fromEntries(this.#sessionAssignments),
|
|
4282
5470
|
pinnedAssignments: Object.fromEntries(this.#pinnedAssignments)
|
|
4283
5471
|
};
|
|
4284
|
-
await
|
|
4285
|
-
await
|
|
5472
|
+
await mkdir9(path16.dirname(this.#storePath), { recursive: true });
|
|
5473
|
+
await writeFile10(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
4286
5474
|
}
|
|
4287
5475
|
};
|
|
4288
5476
|
function isWorkspaceFolder(value) {
|
|
@@ -4296,11 +5484,13 @@ function isPinnedFolder(value) {
|
|
|
4296
5484
|
}
|
|
4297
5485
|
|
|
4298
5486
|
// ../host/dist/app-settings.js
|
|
4299
|
-
import { mkdir as
|
|
5487
|
+
import { mkdir as mkdir10, readFile as readFile12, writeFile as writeFile11 } from "node:fs/promises";
|
|
4300
5488
|
import { homedir as homedir4 } from "node:os";
|
|
4301
|
-
import
|
|
5489
|
+
import path17 from "node:path";
|
|
4302
5490
|
var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
4303
5491
|
var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
|
|
5492
|
+
var DEFAULT_AUTO_STOP_AFTER_MS = 10 * 60 * 1e3;
|
|
5493
|
+
var MIN_AUTO_STOP_AFTER_MS = 60 * 1e3;
|
|
4304
5494
|
var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
|
|
4305
5495
|
var FOLDER_ACCESS = ["all", "projects", "project"];
|
|
4306
5496
|
var PERMISSION_MODES = [
|
|
@@ -4313,6 +5503,7 @@ var PERMISSION_MODES = [
|
|
|
4313
5503
|
];
|
|
4314
5504
|
var DEFAULT_APP_SETTINGS = {
|
|
4315
5505
|
autoArchiveAfterMs: DEFAULT_AUTO_ARCHIVE_AFTER_MS,
|
|
5506
|
+
autoStopAfterMs: DEFAULT_AUTO_STOP_AFTER_MS,
|
|
4316
5507
|
// Строгий режим по умолчанию: молча писать в чужой проект приложение начнёт
|
|
4317
5508
|
// только после того, как об этом попросили явно.
|
|
4318
5509
|
folderAccess: "project",
|
|
@@ -4329,17 +5520,22 @@ var DEFAULT_APP_SETTINGS = {
|
|
|
4329
5520
|
// машине что угодно.
|
|
4330
5521
|
terminalEnabled: false,
|
|
4331
5522
|
// Выключено по умолчанию: списки идут в том порядке, в каком их расставили.
|
|
4332
|
-
liveSessionsFirst: false
|
|
5523
|
+
liveSessionsFirst: false,
|
|
5524
|
+
// Пятичасовое окно останавливаем с запасом: хэндофф — это ещё один ход, и
|
|
5525
|
+
// ему нужно место. Недельное — у самого края: до сброса там может быть
|
|
5526
|
+
// несколько дней, и терять их из-за пары процентов жалко.
|
|
5527
|
+
limitStopFiveHourPct: 95,
|
|
5528
|
+
limitStopSevenDayPct: 99
|
|
4333
5529
|
};
|
|
4334
5530
|
var AppSettingsStore = class {
|
|
4335
5531
|
#settings = { ...DEFAULT_APP_SETTINGS };
|
|
4336
5532
|
#storePath;
|
|
4337
5533
|
constructor(dataDir) {
|
|
4338
|
-
this.#storePath =
|
|
5534
|
+
this.#storePath = path17.join(dataDir, "app-settings.json");
|
|
4339
5535
|
}
|
|
4340
5536
|
async load() {
|
|
4341
5537
|
try {
|
|
4342
|
-
const raw = await
|
|
5538
|
+
const raw = await readFile12(this.#storePath, "utf8");
|
|
4343
5539
|
this.#settings = sanitize2(JSON.parse(raw), DEFAULT_APP_SETTINGS);
|
|
4344
5540
|
} catch {
|
|
4345
5541
|
this.#settings = { ...DEFAULT_APP_SETTINGS };
|
|
@@ -4359,8 +5555,8 @@ var AppSettingsStore = class {
|
|
|
4359
5555
|
async update(patch) {
|
|
4360
5556
|
const next = sanitize2(patch, this.#settings);
|
|
4361
5557
|
this.#settings = next;
|
|
4362
|
-
await
|
|
4363
|
-
await
|
|
5558
|
+
await mkdir10(path17.dirname(this.#storePath), { recursive: true });
|
|
5559
|
+
await writeFile11(this.#storePath, JSON.stringify(next, null, 2), "utf8");
|
|
4364
5560
|
return this.current;
|
|
4365
5561
|
}
|
|
4366
5562
|
};
|
|
@@ -4373,6 +5569,10 @@ function sanitize2(value, fallback) {
|
|
|
4373
5569
|
if (typeof after === "number" && Number.isFinite(after)) {
|
|
4374
5570
|
result.autoArchiveAfterMs = after <= 0 ? 0 : Math.max(Math.round(after), MIN_AUTO_ARCHIVE_AFTER_MS);
|
|
4375
5571
|
}
|
|
5572
|
+
const stopAfter = record.autoStopAfterMs;
|
|
5573
|
+
if (typeof stopAfter === "number" && Number.isFinite(stopAfter)) {
|
|
5574
|
+
result.autoStopAfterMs = stopAfter <= 0 ? 0 : Math.max(Math.round(stopAfter), MIN_AUTO_STOP_AFTER_MS);
|
|
5575
|
+
}
|
|
4376
5576
|
const access3 = record.folderAccess;
|
|
4377
5577
|
if (typeof access3 === "string" && FOLDER_ACCESS.includes(access3)) {
|
|
4378
5578
|
result.folderAccess = access3;
|
|
@@ -4398,10 +5598,16 @@ function sanitize2(value, fallback) {
|
|
|
4398
5598
|
if (typeof record.liveSessionsFirst === "boolean") {
|
|
4399
5599
|
result.liveSessionsFirst = record.liveSessionsFirst;
|
|
4400
5600
|
}
|
|
5601
|
+
for (const key of ["limitStopFiveHourPct", "limitStopSevenDayPct"]) {
|
|
5602
|
+
const pct = record[key];
|
|
5603
|
+
if (typeof pct === "number" && Number.isFinite(pct)) {
|
|
5604
|
+
result[key] = pct <= 0 ? 0 : Math.min(Math.max(Math.round(pct), 1), 100);
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
4401
5607
|
return result;
|
|
4402
5608
|
}
|
|
4403
5609
|
function claudeHome() {
|
|
4404
|
-
return
|
|
5610
|
+
return path17.join(homedir4(), ".claude");
|
|
4405
5611
|
}
|
|
4406
5612
|
function writableRoots(mode, cwd, workspacePaths) {
|
|
4407
5613
|
if (mode === "all")
|
|
@@ -4411,17 +5617,189 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
4411
5617
|
return [...roots];
|
|
4412
5618
|
for (const workspace of [cwd, ...workspacePaths]) {
|
|
4413
5619
|
roots.add(workspace);
|
|
4414
|
-
const parent =
|
|
5620
|
+
const parent = path17.dirname(workspace);
|
|
4415
5621
|
if (parent !== workspace)
|
|
4416
5622
|
roots.add(parent);
|
|
4417
5623
|
}
|
|
4418
5624
|
return [...roots];
|
|
4419
5625
|
}
|
|
4420
5626
|
|
|
5627
|
+
// ../host/dist/handoff-store.js
|
|
5628
|
+
import { mkdir as mkdir11, readFile as readFile13, readdir as readdir3, rm as rm4, writeFile as writeFile12 } from "node:fs/promises";
|
|
5629
|
+
import path18 from "node:path";
|
|
5630
|
+
var HandoffStore = class {
|
|
5631
|
+
#dir;
|
|
5632
|
+
constructor(dataDir) {
|
|
5633
|
+
this.#dir = path18.join(dataDir, "handoffs");
|
|
5634
|
+
}
|
|
5635
|
+
async get(sessionId) {
|
|
5636
|
+
const file = this.#file(sessionId);
|
|
5637
|
+
if (!file)
|
|
5638
|
+
return null;
|
|
5639
|
+
try {
|
|
5640
|
+
const parsed = JSON.parse(await readFile13(file, "utf8"));
|
|
5641
|
+
return typeof parsed.text === "string" ? parsed : null;
|
|
5642
|
+
} catch {
|
|
5643
|
+
return null;
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
async save(handoff) {
|
|
5647
|
+
const file = this.#file(handoff.sessionId);
|
|
5648
|
+
if (!file)
|
|
5649
|
+
return;
|
|
5650
|
+
await mkdir11(this.#dir, { recursive: true });
|
|
5651
|
+
await writeFile12(file, JSON.stringify(handoff, null, 2), "utf8");
|
|
5652
|
+
}
|
|
5653
|
+
async remove(sessionId) {
|
|
5654
|
+
const file = this.#file(sessionId);
|
|
5655
|
+
if (file)
|
|
5656
|
+
await rm4(file, { force: true });
|
|
5657
|
+
}
|
|
5658
|
+
/** id всех сохранённых хэндоффов. */
|
|
5659
|
+
async list() {
|
|
5660
|
+
try {
|
|
5661
|
+
return (await readdir3(this.#dir)).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length));
|
|
5662
|
+
} catch {
|
|
5663
|
+
return [];
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
/** Путь файла; `null` — id пришёл от клиента и на имя файла не годится. */
|
|
5667
|
+
#file(sessionId) {
|
|
5668
|
+
if (!/^[\w-]{1,128}$/.test(sessionId))
|
|
5669
|
+
return null;
|
|
5670
|
+
return path18.join(this.#dir, `${sessionId}.json`);
|
|
5671
|
+
}
|
|
5672
|
+
};
|
|
5673
|
+
var LimitStopState = class {
|
|
5674
|
+
#fired = {};
|
|
5675
|
+
#storePath;
|
|
5676
|
+
constructor(dataDir) {
|
|
5677
|
+
this.#storePath = path18.join(dataDir, "limit-stop.json");
|
|
5678
|
+
}
|
|
5679
|
+
async load() {
|
|
5680
|
+
try {
|
|
5681
|
+
const parsed = JSON.parse(await readFile13(this.#storePath, "utf8"));
|
|
5682
|
+
this.#fired = {};
|
|
5683
|
+
for (const key of ["fiveHour", "sevenDay"]) {
|
|
5684
|
+
const until = parsed[key];
|
|
5685
|
+
if (typeof until === "number" && Number.isFinite(until))
|
|
5686
|
+
this.#fired[key] = until;
|
|
5687
|
+
}
|
|
5688
|
+
} catch {
|
|
5689
|
+
this.#fired = {};
|
|
5690
|
+
}
|
|
5691
|
+
}
|
|
5692
|
+
/** Окно `limit` уже сработало и ещё не сбросилось. */
|
|
5693
|
+
fired(limit, now = Date.now()) {
|
|
5694
|
+
return (this.#fired[limit] ?? 0) > now;
|
|
5695
|
+
}
|
|
5696
|
+
/** Запомнить срабатывание до `until` (момент сброса окна). */
|
|
5697
|
+
async mark(limit, until) {
|
|
5698
|
+
this.#fired[limit] = until;
|
|
5699
|
+
await mkdir11(path18.dirname(this.#storePath), { recursive: true });
|
|
5700
|
+
await writeFile12(this.#storePath, JSON.stringify(this.#fired, null, 2), "utf8");
|
|
5701
|
+
}
|
|
5702
|
+
};
|
|
5703
|
+
|
|
5704
|
+
// ../host/dist/limit-guard.js
|
|
5705
|
+
var HANDOFF_TIMEOUT_MS = 3 * 6e4;
|
|
5706
|
+
var LIMIT_SWEEP_MS = 6e4;
|
|
5707
|
+
var WINDOW_SPAN_MS = {
|
|
5708
|
+
fiveHour: 5 * 60 * 6e4,
|
|
5709
|
+
sevenDay: 7 * 24 * 60 * 6e4
|
|
5710
|
+
};
|
|
5711
|
+
var LIMIT_LABEL = {
|
|
5712
|
+
fiveHour: "5-\u0447\u0430\u0441\u043E\u0432\u043E\u0439 \u043B\u0438\u043C\u0438\u0442",
|
|
5713
|
+
sevenDay: "\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442"
|
|
5714
|
+
};
|
|
5715
|
+
function crossedLimit(usage, settings, fired, now = Date.now()) {
|
|
5716
|
+
if (!usage.available)
|
|
5717
|
+
return null;
|
|
5718
|
+
const checks = [
|
|
5719
|
+
["fiveHour", usage.fiveHour, settings.limitStopFiveHourPct],
|
|
5720
|
+
["sevenDay", usage.sevenDay, settings.limitStopSevenDayPct]
|
|
5721
|
+
];
|
|
5722
|
+
for (const [limit, window, threshold] of checks) {
|
|
5723
|
+
if (!window || threshold <= 0 || window.utilization < threshold)
|
|
5724
|
+
continue;
|
|
5725
|
+
if (fired(limit))
|
|
5726
|
+
continue;
|
|
5727
|
+
const resets = Date.parse(window.resetsAt);
|
|
5728
|
+
const until = Number.isFinite(resets) && resets > now ? resets : now + WINDOW_SPAN_MS[limit];
|
|
5729
|
+
return { limit, window, until };
|
|
5730
|
+
}
|
|
5731
|
+
return null;
|
|
5732
|
+
}
|
|
5733
|
+
function handoffPrompt(hit) {
|
|
5734
|
+
return [
|
|
5735
|
+
`[ccv] ${capitalize(LIMIT_LABEL[hit.limit])} \u043F\u043E\u0447\u0442\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${Math.round(hit.window.utilization)} %), \u0440\u0430\u0431\u043E\u0442\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430.`,
|
|
5736
|
+
"\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u0432 \u044D\u0442\u043E\u043C \u0445\u043E\u0434\u0435 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u044B \u2014 \u043D\u0435 \u0432\u044B\u0437\u044B\u0432\u0430\u0439 \u0438\u0445.",
|
|
5737
|
+
"\u041E\u0442\u0432\u0435\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435\u043C \u2014 \u0445\u044D\u043D\u0434\u043E\u0444\u0444\u043E\u043C, \u0441 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0430\u0431\u043E\u0442\u0443 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442 \u0432 \u043D\u043E\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438.",
|
|
5738
|
+
"Markdown, \u0440\u0430\u0437\u0434\u0435\u043B\u044B: \xAB## \u0426\u0435\u043B\u044C\xBB, \xAB## \u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435\xBB (\u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u043D\u043E, \u0447\u0442\u043E \u043D\u0435\u0442, \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u043E),",
|
|
5739
|
+
"\xAB## \u0422\u0440\u043E\u043D\u0443\u0442\u044B\u0435 \u0444\u0430\u0439\u043B\u044B\xBB, \xAB## \u041E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B\xBB, \xAB## \u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0448\u0430\u0433\xBB.",
|
|
5740
|
+
"\u041A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E: \u043F\u0443\u0442\u0438, \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F. \u0411\u0435\u0437 \u0432\u0441\u0442\u0443\u043F\u043B\u0435\u043D\u0438\u0439."
|
|
5741
|
+
].join("\n");
|
|
5742
|
+
}
|
|
5743
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
5744
|
+
function journalHandoff(entries) {
|
|
5745
|
+
const prompts = [];
|
|
5746
|
+
let lastText = "";
|
|
5747
|
+
let todos = null;
|
|
5748
|
+
const files = /* @__PURE__ */ new Set();
|
|
5749
|
+
for (const { event } of entries) {
|
|
5750
|
+
if (event.kind === "user.message") {
|
|
5751
|
+
if (!event.text.startsWith("[ccv]"))
|
|
5752
|
+
prompts.push(event.text);
|
|
5753
|
+
} else if (event.kind === "assistant.text") {
|
|
5754
|
+
if (event.text.trim())
|
|
5755
|
+
lastText = event.text;
|
|
5756
|
+
} else if (event.kind === "tool.use") {
|
|
5757
|
+
const input = event.input ?? {};
|
|
5758
|
+
if (EDIT_TOOLS.has(event.name)) {
|
|
5759
|
+
const target = input.file_path ?? input.notebook_path;
|
|
5760
|
+
if (typeof target === "string")
|
|
5761
|
+
files.add(target);
|
|
5762
|
+
} else if (event.name === "TodoWrite" && Array.isArray(input.todos)) {
|
|
5763
|
+
todos = input.todos.flatMap((todo) => {
|
|
5764
|
+
const t = todo;
|
|
5765
|
+
return typeof t.content === "string" ? [{ content: t.content, status: String(t.status ?? "") }] : [];
|
|
5766
|
+
});
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
const sections = [
|
|
5771
|
+
"_\u0425\u044D\u043D\u0434\u043E\u0444\u0444 \u0441\u043E\u0431\u0440\u0430\u043D \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0438\u0437 \u0436\u0443\u0440\u043D\u0430\u043B\u0430 \u0441\u0435\u0441\u0441\u0438\u0438: \u0430\u0433\u0435\u043D\u0442 \u043D\u0435 \u0443\u0441\u043F\u0435\u043B \u043D\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0435\u0433\u043E \u0441\u0430\u043C._"
|
|
5772
|
+
];
|
|
5773
|
+
if (prompts.length > 0) {
|
|
5774
|
+
sections.push("## \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B\n\n" + prompts.slice(-3).map((text) => quote(clip2(text, 1500))).join("\n\n"));
|
|
5775
|
+
}
|
|
5776
|
+
if (todos && todos.length > 0) {
|
|
5777
|
+
const mark = (status) => status === "completed" ? "[x]" : status === "in_progress" ? "[~]" : "[ ]";
|
|
5778
|
+
sections.push("## \u0417\u0430\u0434\u0430\u0447\u0438\n\n" + todos.map((t) => `- ${mark(t.status)} ${t.content}`).join("\n"));
|
|
5779
|
+
}
|
|
5780
|
+
if (files.size > 0) {
|
|
5781
|
+
sections.push("## \u0422\u0440\u043E\u043D\u0443\u0442\u044B\u0435 \u0444\u0430\u0439\u043B\u044B\n\n" + [...files].map((f) => `- \`${f}\``).join("\n"));
|
|
5782
|
+
}
|
|
5783
|
+
if (lastText)
|
|
5784
|
+
sections.push("## \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u043E\u0442\u0432\u0435\u0442 \u0430\u0433\u0435\u043D\u0442\u0430\n\n" + clip2(lastText, 4e3));
|
|
5785
|
+
if (sections.length === 1)
|
|
5786
|
+
sections.push("\u0412 \u0436\u0443\u0440\u043D\u0430\u043B\u0435 \u044D\u0442\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u043B\u043E\u0441\u044C.");
|
|
5787
|
+
return sections.join("\n\n");
|
|
5788
|
+
}
|
|
5789
|
+
function clip2(text, max) {
|
|
5790
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
5791
|
+
}
|
|
5792
|
+
function quote(text) {
|
|
5793
|
+
return text.split("\n").map((line) => `> ${line}`).join("\n");
|
|
5794
|
+
}
|
|
5795
|
+
function capitalize(text) {
|
|
5796
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
5797
|
+
}
|
|
5798
|
+
|
|
4421
5799
|
// ../host/dist/plugins.js
|
|
4422
|
-
import { readFile as
|
|
5800
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
4423
5801
|
import { homedir as homedir5 } from "node:os";
|
|
4424
|
-
import
|
|
5802
|
+
import path19 from "node:path";
|
|
4425
5803
|
var TIMEOUT_MS2 = 12e4;
|
|
4426
5804
|
var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
|
|
4427
5805
|
var PluginCli = class {
|
|
@@ -4520,10 +5898,10 @@ async function readCatalog(markets, installed) {
|
|
|
4520
5898
|
return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
4521
5899
|
}
|
|
4522
5900
|
async function readMarketplace(market, installed) {
|
|
4523
|
-
const root = market.installLocation ||
|
|
5901
|
+
const root = market.installLocation || path19.join(homedir5(), ".claude", "plugins", "marketplaces", market.name);
|
|
4524
5902
|
let manifest;
|
|
4525
5903
|
try {
|
|
4526
|
-
const text = await
|
|
5904
|
+
const text = await readFile14(path19.join(root, ".claude-plugin", "marketplace.json"), "utf8");
|
|
4527
5905
|
manifest = JSON.parse(text);
|
|
4528
5906
|
} catch {
|
|
4529
5907
|
return [];
|
|
@@ -4566,7 +5944,7 @@ function authorName(raw) {
|
|
|
4566
5944
|
}
|
|
4567
5945
|
|
|
4568
5946
|
// ../host/dist/auth.js
|
|
4569
|
-
import { EventEmitter as
|
|
5947
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
4570
5948
|
var QUICK_TIMEOUT_MS = 3e4;
|
|
4571
5949
|
var LOGIN_TIMEOUT_MS = 10 * 6e4;
|
|
4572
5950
|
var URL_RE = /https:\/\/\S*\/oauth\/authorize\S*/;
|
|
@@ -4578,7 +5956,7 @@ var IDLE = {
|
|
|
4578
5956
|
output: "",
|
|
4579
5957
|
error: null
|
|
4580
5958
|
};
|
|
4581
|
-
var AuthCli = class extends
|
|
5959
|
+
var AuthCli = class extends EventEmitter3 {
|
|
4582
5960
|
#child = null;
|
|
4583
5961
|
#state = IDLE;
|
|
4584
5962
|
#timer = null;
|
|
@@ -4610,7 +5988,7 @@ var AuthCli = class extends EventEmitter2 {
|
|
|
4610
5988
|
if (this.#child)
|
|
4611
5989
|
throw new Error("\u0412\u0445\u043E\u0434 \u0443\u0436\u0435 \u0438\u0434\u0451\u0442 \u2014 \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u0435 \u0435\u0433\u043E");
|
|
4612
5990
|
if (switchAccount) {
|
|
4613
|
-
const out = await run(["auth", "logout"], QUICK_TIMEOUT_MS).catch(
|
|
5991
|
+
const out = await run(["auth", "logout"], QUICK_TIMEOUT_MS).catch(describe5);
|
|
4614
5992
|
this.#push({ ...IDLE, active: true, output: `${out}
|
|
4615
5993
|
` });
|
|
4616
5994
|
this.emit("changed");
|
|
@@ -4630,7 +6008,7 @@ var AuthCli = class extends EventEmitter2 {
|
|
|
4630
6008
|
child.stdout?.on("data", (chunk) => this.#absorb(chunk.toString()));
|
|
4631
6009
|
child.stderr?.on("data", (chunk) => this.#absorb(chunk.toString()));
|
|
4632
6010
|
child.on("error", (error) => {
|
|
4633
|
-
this.#fail(error.code === "ENOENT" ? "\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D claude \u0432 PATH \u2014 \u0432\u0445\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" :
|
|
6011
|
+
this.#fail(error.code === "ENOENT" ? "\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D claude \u0432 PATH \u2014 \u0432\u0445\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" : describe5(error));
|
|
4634
6012
|
});
|
|
4635
6013
|
child.on("close", (code) => {
|
|
4636
6014
|
if (this.#child !== child)
|
|
@@ -4758,14 +6136,14 @@ function parseStatus2(raw) {
|
|
|
4758
6136
|
function str2(value) {
|
|
4759
6137
|
return typeof value === "string" ? value : "";
|
|
4760
6138
|
}
|
|
4761
|
-
function
|
|
6139
|
+
function describe5(error) {
|
|
4762
6140
|
return error instanceof Error ? error.message : String(error);
|
|
4763
6141
|
}
|
|
4764
6142
|
|
|
4765
6143
|
// ../host/dist/relay-link.js
|
|
4766
|
-
import { EventEmitter as
|
|
4767
|
-
import { readFile as
|
|
4768
|
-
import
|
|
6144
|
+
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
6145
|
+
import { readFile as readFile15, writeFile as writeFile13, rm as rm5, mkdir as mkdir12 } from "node:fs/promises";
|
|
6146
|
+
import path20 from "node:path";
|
|
4769
6147
|
import { hostname } from "node:os";
|
|
4770
6148
|
import WebSocket from "ws";
|
|
4771
6149
|
|
|
@@ -4782,7 +6160,7 @@ var SILENCE_TIMEOUT_MS = 95e3;
|
|
|
4782
6160
|
function jitter(delay) {
|
|
4783
6161
|
return Math.round(delay * (0.8 + Math.random() * 0.4));
|
|
4784
6162
|
}
|
|
4785
|
-
var RelayLink = class extends
|
|
6163
|
+
var RelayLink = class extends EventEmitter4 {
|
|
4786
6164
|
#credentials = null;
|
|
4787
6165
|
#socket = null;
|
|
4788
6166
|
#retry = RETRY_MIN_MS;
|
|
@@ -4820,7 +6198,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4820
6198
|
#log;
|
|
4821
6199
|
constructor(options2) {
|
|
4822
6200
|
super();
|
|
4823
|
-
this.#file =
|
|
6201
|
+
this.#file = path20.join(options2.dataDir, "relay.json");
|
|
4824
6202
|
this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
|
|
4825
6203
|
this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
|
|
4826
6204
|
}
|
|
@@ -4901,7 +6279,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4901
6279
|
}
|
|
4902
6280
|
async #read() {
|
|
4903
6281
|
try {
|
|
4904
|
-
const raw = await
|
|
6282
|
+
const raw = await readFile15(this.#file, "utf8");
|
|
4905
6283
|
const parsed = JSON.parse(raw);
|
|
4906
6284
|
if (typeof parsed !== "object" || parsed === null)
|
|
4907
6285
|
return null;
|
|
@@ -4914,8 +6292,8 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4914
6292
|
}
|
|
4915
6293
|
}
|
|
4916
6294
|
async #write(credentials) {
|
|
4917
|
-
await
|
|
4918
|
-
await
|
|
6295
|
+
await mkdir12(path20.dirname(this.#file), { recursive: true });
|
|
6296
|
+
await writeFile13(this.#file, `${JSON.stringify(credentials, null, 2)}
|
|
4919
6297
|
`, {
|
|
4920
6298
|
encoding: "utf8",
|
|
4921
6299
|
mode: 384
|
|
@@ -4939,7 +6317,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4939
6317
|
body: JSON.stringify({ code, name: machine, platform: process.platform })
|
|
4940
6318
|
});
|
|
4941
6319
|
} catch (error) {
|
|
4942
|
-
throw new Error(`\u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u0434\u043E ${url2}: ${
|
|
6320
|
+
throw new Error(`\u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u0434\u043E ${url2}: ${describe6(error)}`);
|
|
4943
6321
|
}
|
|
4944
6322
|
const data = await response.json().catch(() => ({}));
|
|
4945
6323
|
if (!response.ok || !data.token || !data.hostId) {
|
|
@@ -4967,7 +6345,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4967
6345
|
this.#error = null;
|
|
4968
6346
|
this.#clients.clear();
|
|
4969
6347
|
this.#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
|
|
4970
|
-
await
|
|
6348
|
+
await rm5(this.#file, { force: true });
|
|
4971
6349
|
this.#changed();
|
|
4972
6350
|
this.#log("\u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435 \u0441\u043D\u044F\u0442\u043E");
|
|
4973
6351
|
}
|
|
@@ -5158,11 +6536,11 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
5158
6536
|
socket.send(JSON.stringify(frame));
|
|
5159
6537
|
}
|
|
5160
6538
|
};
|
|
5161
|
-
function
|
|
6539
|
+
function describe6(error) {
|
|
5162
6540
|
return error instanceof Error ? error.message : String(error);
|
|
5163
6541
|
}
|
|
5164
6542
|
function humanize(error) {
|
|
5165
|
-
const message =
|
|
6543
|
+
const message = describe6(error);
|
|
5166
6544
|
if (/handshake has timed out/i.test(message)) {
|
|
5167
6545
|
return `relay \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043D\u0430 \u0440\u0443\u043A\u043E\u043F\u043E\u0436\u0430\u0442\u0438\u0435 \u0437\u0430 ${Math.round(HANDSHAKE_TIMEOUT_MS / 1e3)} \u0441 \u2014 \u043F\u0440\u043E\u0431\u0443\u0435\u043C \u0441\u043D\u043E\u0432\u0430`;
|
|
5168
6546
|
}
|
|
@@ -5170,14 +6548,14 @@ function humanize(error) {
|
|
|
5170
6548
|
}
|
|
5171
6549
|
|
|
5172
6550
|
// ../host/dist/terminal.js
|
|
5173
|
-
import { EventEmitter as
|
|
6551
|
+
import { EventEmitter as EventEmitter5 } from "node:events";
|
|
5174
6552
|
import { access as access2, constants as constants2 } from "node:fs/promises";
|
|
5175
|
-
import
|
|
5176
|
-
import { randomUUID as
|
|
6553
|
+
import path21 from "node:path";
|
|
6554
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5177
6555
|
var SCROLLBACK_BYTES = 256 * 1024;
|
|
5178
6556
|
var FLUSH_MS = 12;
|
|
5179
6557
|
var MAX_TERMINALS = 24;
|
|
5180
|
-
var TerminalHub = class extends
|
|
6558
|
+
var TerminalHub = class extends EventEmitter5 {
|
|
5181
6559
|
#terminals = /* @__PURE__ */ new Map();
|
|
5182
6560
|
#module = null;
|
|
5183
6561
|
list(workspaceId) {
|
|
@@ -5210,7 +6588,7 @@ var TerminalHub = class extends EventEmitter4 {
|
|
|
5210
6588
|
env: environment()
|
|
5211
6589
|
});
|
|
5212
6590
|
const info = {
|
|
5213
|
-
id:
|
|
6591
|
+
id: randomUUID4(),
|
|
5214
6592
|
workspaceId: options2.workspaceId,
|
|
5215
6593
|
title: shell.title,
|
|
5216
6594
|
shell: shell.file,
|
|
@@ -5342,22 +6720,22 @@ async function defaultShell() {
|
|
|
5342
6720
|
if (pwsh)
|
|
5343
6721
|
return { file: pwsh, args: [], title: "pwsh" };
|
|
5344
6722
|
const system = process.env.SystemRoot ?? "C:\\Windows";
|
|
5345
|
-
const powershell =
|
|
6723
|
+
const powershell = path21.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
|
|
5346
6724
|
if (await exists2(powershell))
|
|
5347
6725
|
return { file: powershell, args: [], title: "powershell" };
|
|
5348
6726
|
return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
|
|
5349
6727
|
}
|
|
5350
6728
|
const shell = process.env.SHELL;
|
|
5351
6729
|
if (shell && await exists2(shell))
|
|
5352
|
-
return { file: shell, args: [], title:
|
|
6730
|
+
return { file: shell, args: [], title: path21.basename(shell) };
|
|
5353
6731
|
if (await exists2("/bin/bash"))
|
|
5354
6732
|
return { file: "/bin/bash", args: [], title: "bash" };
|
|
5355
6733
|
return { file: "/bin/sh", args: [], title: "sh" };
|
|
5356
6734
|
}
|
|
5357
6735
|
async function findOnPath(name) {
|
|
5358
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
6736
|
+
const dirs = (process.env.PATH ?? "").split(path21.delimiter).filter(Boolean);
|
|
5359
6737
|
for (const dir of dirs) {
|
|
5360
|
-
const candidate =
|
|
6738
|
+
const candidate = path21.join(dir, name);
|
|
5361
6739
|
if (await exists2(candidate))
|
|
5362
6740
|
return candidate;
|
|
5363
6741
|
}
|
|
@@ -5368,10 +6746,10 @@ function exists2(file) {
|
|
|
5368
6746
|
}
|
|
5369
6747
|
|
|
5370
6748
|
// ../host/dist/mcp-config.js
|
|
5371
|
-
import { readFile as
|
|
5372
|
-
import
|
|
6749
|
+
import { readFile as readFile16, writeFile as writeFile14, mkdir as mkdir13 } from "node:fs/promises";
|
|
6750
|
+
import path22 from "node:path";
|
|
5373
6751
|
var MCP_FILE = ".mcp.json";
|
|
5374
|
-
var LOCAL_SETTINGS2 =
|
|
6752
|
+
var LOCAL_SETTINGS2 = path22.join(".claude", "settings.local.json");
|
|
5375
6753
|
var queues = /* @__PURE__ */ new Map();
|
|
5376
6754
|
function serialize(cwd, work) {
|
|
5377
6755
|
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
@@ -5380,7 +6758,7 @@ function serialize(cwd, work) {
|
|
|
5380
6758
|
return next;
|
|
5381
6759
|
}
|
|
5382
6760
|
async function readProjectServers(cwd) {
|
|
5383
|
-
const file = await readJson(
|
|
6761
|
+
const file = await readJson(path22.join(cwd, MCP_FILE));
|
|
5384
6762
|
const servers = file?.mcpServers;
|
|
5385
6763
|
if (!servers || typeof servers !== "object")
|
|
5386
6764
|
return {};
|
|
@@ -5388,23 +6766,23 @@ async function readProjectServers(cwd) {
|
|
|
5388
6766
|
}
|
|
5389
6767
|
function addProjectServer(cwd, name, config) {
|
|
5390
6768
|
return serialize(cwd, async () => {
|
|
5391
|
-
const file = await readJson(
|
|
6769
|
+
const file = await readJson(path22.join(cwd, MCP_FILE)) ?? {};
|
|
5392
6770
|
const servers = file.mcpServers ?? {};
|
|
5393
6771
|
servers[name] = config;
|
|
5394
6772
|
file.mcpServers = servers;
|
|
5395
|
-
await writeJson(
|
|
6773
|
+
await writeJson(path22.join(cwd, MCP_FILE), file);
|
|
5396
6774
|
await approve(cwd, name);
|
|
5397
6775
|
});
|
|
5398
6776
|
}
|
|
5399
6777
|
function removeProjectServer(cwd, name) {
|
|
5400
6778
|
return serialize(cwd, async () => {
|
|
5401
|
-
const file = await readJson(
|
|
6779
|
+
const file = await readJson(path22.join(cwd, MCP_FILE));
|
|
5402
6780
|
const servers = file?.mcpServers;
|
|
5403
6781
|
if (!servers || !(name in servers))
|
|
5404
6782
|
return false;
|
|
5405
6783
|
delete servers[name];
|
|
5406
|
-
await writeJson(
|
|
5407
|
-
const settingsPath =
|
|
6784
|
+
await writeJson(path22.join(cwd, MCP_FILE), file);
|
|
6785
|
+
const settingsPath = path22.join(cwd, LOCAL_SETTINGS2);
|
|
5408
6786
|
const settings = await readJson(settingsPath);
|
|
5409
6787
|
const enabled = settings?.enabledMcpjsonServers;
|
|
5410
6788
|
if (settings && Array.isArray(enabled) && enabled.includes(name)) {
|
|
@@ -5415,7 +6793,7 @@ function removeProjectServer(cwd, name) {
|
|
|
5415
6793
|
});
|
|
5416
6794
|
}
|
|
5417
6795
|
async function approve(cwd, name) {
|
|
5418
|
-
const file =
|
|
6796
|
+
const file = path22.join(cwd, LOCAL_SETTINGS2);
|
|
5419
6797
|
const settings = await readJson(file) ?? {};
|
|
5420
6798
|
const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
5421
6799
|
if (!enabled.includes(name))
|
|
@@ -5454,21 +6832,21 @@ function describeConfig(config) {
|
|
|
5454
6832
|
}
|
|
5455
6833
|
async function readJson(file) {
|
|
5456
6834
|
try {
|
|
5457
|
-
const raw = await
|
|
6835
|
+
const raw = await readFile16(file, "utf8");
|
|
5458
6836
|
const parsed = JSON.parse(raw);
|
|
5459
6837
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
5460
6838
|
} catch (error) {
|
|
5461
6839
|
if (error.code === "ENOENT")
|
|
5462
6840
|
return null;
|
|
5463
6841
|
if (error instanceof SyntaxError) {
|
|
5464
|
-
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${
|
|
6842
|
+
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path22.basename(file)}: ${error.message}`);
|
|
5465
6843
|
}
|
|
5466
6844
|
throw error;
|
|
5467
6845
|
}
|
|
5468
6846
|
}
|
|
5469
6847
|
async function writeJson(file, data) {
|
|
5470
|
-
await
|
|
5471
|
-
await
|
|
6848
|
+
await mkdir13(path22.dirname(file), { recursive: true });
|
|
6849
|
+
await writeFile14(file, `${JSON.stringify(data, null, 2)}
|
|
5472
6850
|
`, "utf8");
|
|
5473
6851
|
}
|
|
5474
6852
|
|
|
@@ -5477,6 +6855,12 @@ var SIDE_TITLE = "\xB7 \u0432\u043E\u043F\u0440\u043E\u0441 \u043D\u0430 \u043F\
|
|
|
5477
6855
|
var FINDER_TITLE = "\xB7 \u043F\u043E\u0438\u0441\u043A \u043F\u0440\u043E\u0435\u043A\u0442\u0430";
|
|
5478
6856
|
var HISTORY_PAGE = 250;
|
|
5479
6857
|
var HISTORY_PAGE_MAX = 500;
|
|
6858
|
+
var AUTO_STOP_SWEEP_MS = 30 * 1e3;
|
|
6859
|
+
var BUSY_STATES = /* @__PURE__ */ new Set([
|
|
6860
|
+
"running",
|
|
6861
|
+
"waiting_permission",
|
|
6862
|
+
"reconnecting"
|
|
6863
|
+
]);
|
|
5480
6864
|
var HostServer = class {
|
|
5481
6865
|
#options;
|
|
5482
6866
|
#workspaces;
|
|
@@ -5487,6 +6871,7 @@ var HostServer = class {
|
|
|
5487
6871
|
#settings;
|
|
5488
6872
|
#finder;
|
|
5489
6873
|
#sessions = /* @__PURE__ */ new Map();
|
|
6874
|
+
#antigravity;
|
|
5490
6875
|
/** Боковые сессии («вопрос на полях») по id родителя — не больше одной на разговор. */
|
|
5491
6876
|
#sides = /* @__PURE__ */ new Map();
|
|
5492
6877
|
/**
|
|
@@ -5501,7 +6886,27 @@ var HostServer = class {
|
|
|
5501
6886
|
#clients = /* @__PURE__ */ new Set();
|
|
5502
6887
|
/** Удалённые вкладки по их id в relay — чтобы адресовать ответ именно той. */
|
|
5503
6888
|
#remotes = /* @__PURE__ */ new Map();
|
|
5504
|
-
|
|
6889
|
+
/**
|
|
6890
|
+
* Сессии с недописанным черновиком — по окнам (кадр `session.drafts`).
|
|
6891
|
+
*
|
|
6892
|
+
* По окнам, а не одним множеством: черновик держит сессию, пока окно с ним
|
|
6893
|
+
* подключено, и уйти вместе с окном он должен сам, не дожидаясь, пока
|
|
6894
|
+
* кто-то пришлёт пустой список.
|
|
6895
|
+
*/
|
|
6896
|
+
#drafts = /* @__PURE__ */ new Map();
|
|
6897
|
+
/**
|
|
6898
|
+
* Последнее движение черновика по сессии — появился, исчез, окно с ним
|
|
6899
|
+
* отключилось. Отсчёт простоя идёт от позднего из этого и событий самой
|
|
6900
|
+
* сессии: стёртый черновик — тоже действие человека в ней.
|
|
6901
|
+
*/
|
|
6902
|
+
#draftTouchedAt = /* @__PURE__ */ new Map();
|
|
6903
|
+
#autoStopTimer = null;
|
|
6904
|
+
#handoffs;
|
|
6905
|
+
#limitStop;
|
|
6906
|
+
#limitTimer = null;
|
|
6907
|
+
/** Идёт остановка у лимита — второй снимок лимитов её не запускает. */
|
|
6908
|
+
#limitStopping = false;
|
|
6909
|
+
#hostId = randomUUID5();
|
|
5505
6910
|
#plugins = new PluginCli();
|
|
5506
6911
|
#auth = new AuthCli();
|
|
5507
6912
|
#relay;
|
|
@@ -5524,6 +6929,9 @@ var HostServer = class {
|
|
|
5524
6929
|
this.#order = new ListOrderStore(options2.dataDir);
|
|
5525
6930
|
this.#folders = new Folders(options2.dataDir);
|
|
5526
6931
|
this.#settings = new AppSettingsStore(options2.dataDir);
|
|
6932
|
+
this.#antigravity = new AntigravityStore(options2.dataDir);
|
|
6933
|
+
this.#handoffs = new HandoffStore(options2.dataDir);
|
|
6934
|
+
this.#limitStop = new LimitStopState(options2.dataDir);
|
|
5527
6935
|
this.#finder = new ProjectFinder(this.#workspaces);
|
|
5528
6936
|
this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
|
|
5529
6937
|
this.#github = new GitHubAccount(options2.dataDir);
|
|
@@ -5543,6 +6951,7 @@ var HostServer = class {
|
|
|
5543
6951
|
await this.#order.load();
|
|
5544
6952
|
await this.#folders.load();
|
|
5545
6953
|
await this.#settings.load();
|
|
6954
|
+
await this.#limitStop.load();
|
|
5546
6955
|
await this.#github.load();
|
|
5547
6956
|
await warmClaudeLauncher();
|
|
5548
6957
|
void this.#finder.warm();
|
|
@@ -5565,8 +6974,18 @@ var HostServer = class {
|
|
|
5565
6974
|
await this.#relay.start().catch((error) => {
|
|
5566
6975
|
console.error("[relay]", error instanceof Error ? error.message : String(error));
|
|
5567
6976
|
});
|
|
6977
|
+
this.#autoStopTimer = setInterval(() => void this.#stopIdleSessions(), AUTO_STOP_SWEEP_MS);
|
|
6978
|
+
this.#autoStopTimer.unref();
|
|
6979
|
+
this.#limitTimer = setInterval(() => this.#sweepUsage(), LIMIT_SWEEP_MS);
|
|
6980
|
+
this.#limitTimer.unref();
|
|
5568
6981
|
}
|
|
5569
6982
|
async shutdown() {
|
|
6983
|
+
if (this.#autoStopTimer)
|
|
6984
|
+
clearInterval(this.#autoStopTimer);
|
|
6985
|
+
this.#autoStopTimer = null;
|
|
6986
|
+
if (this.#limitTimer)
|
|
6987
|
+
clearInterval(this.#limitTimer);
|
|
6988
|
+
this.#limitTimer = null;
|
|
5570
6989
|
this.#auth.cancel();
|
|
5571
6990
|
this.#terminals.closeAll();
|
|
5572
6991
|
await this.#relay.close();
|
|
@@ -5586,7 +7005,7 @@ var HostServer = class {
|
|
|
5586
7005
|
}
|
|
5587
7006
|
};
|
|
5588
7007
|
this.#clients.add(client);
|
|
5589
|
-
socket.on("close", () => this.#
|
|
7008
|
+
socket.on("close", () => this.#forgetClient(client));
|
|
5590
7009
|
socket.on("message", (raw) => {
|
|
5591
7010
|
let parsed;
|
|
5592
7011
|
try {
|
|
@@ -5617,7 +7036,7 @@ var HostServer = class {
|
|
|
5617
7036
|
if (!client)
|
|
5618
7037
|
return;
|
|
5619
7038
|
this.#remotes.delete(clientId);
|
|
5620
|
-
this.#
|
|
7039
|
+
this.#forgetClient(client);
|
|
5621
7040
|
});
|
|
5622
7041
|
this.#relay.on("frame", (clientId, body) => {
|
|
5623
7042
|
const client = this.#remotes.get(clientId);
|
|
@@ -5800,6 +7219,22 @@ var HostServer = class {
|
|
|
5800
7219
|
case "session.close":
|
|
5801
7220
|
await this.#dropSession(frame.sessionId, "closed by client");
|
|
5802
7221
|
return;
|
|
7222
|
+
case "session.drafts": {
|
|
7223
|
+
const next = new Set(frame.sessionIds.filter((id) => typeof id === "string"));
|
|
7224
|
+
const previous = this.#drafts.get(socket) ?? /* @__PURE__ */ new Set();
|
|
7225
|
+
const now = Date.now();
|
|
7226
|
+
for (const id of next)
|
|
7227
|
+
if (!previous.has(id))
|
|
7228
|
+
this.#draftTouchedAt.set(id, now);
|
|
7229
|
+
for (const id of previous)
|
|
7230
|
+
if (!next.has(id))
|
|
7231
|
+
this.#draftTouchedAt.set(id, now);
|
|
7232
|
+
if (next.size > 0)
|
|
7233
|
+
this.#drafts.set(socket, next);
|
|
7234
|
+
else
|
|
7235
|
+
this.#drafts.delete(socket);
|
|
7236
|
+
return;
|
|
7237
|
+
}
|
|
5803
7238
|
case "session.reconnect":
|
|
5804
7239
|
this.#requireSession(frame.sessionId).reconnect("\u043F\u043E \u043F\u0440\u043E\u0441\u044C\u0431\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F");
|
|
5805
7240
|
return;
|
|
@@ -6289,6 +7724,17 @@ var HostServer = class {
|
|
|
6289
7724
|
case "settings.get":
|
|
6290
7725
|
this.#send(socket, { type: "settings.state", settings: this.#settings.current });
|
|
6291
7726
|
return;
|
|
7727
|
+
case "handoff.get":
|
|
7728
|
+
this.#send(socket, {
|
|
7729
|
+
type: "handoff.state",
|
|
7730
|
+
sessionId: frame.sessionId,
|
|
7731
|
+
handoff: await this.#handoffs.get(frame.sessionId)
|
|
7732
|
+
});
|
|
7733
|
+
return;
|
|
7734
|
+
case "handoff.dismiss":
|
|
7735
|
+
await this.#handoffs.remove(frame.sessionId);
|
|
7736
|
+
this.#broadcast({ type: "handoff.state", sessionId: frame.sessionId, handoff: null });
|
|
7737
|
+
return;
|
|
6292
7738
|
case "settings.update": {
|
|
6293
7739
|
const before = this.#settings.current;
|
|
6294
7740
|
const settings = await this.#settings.update(frame.settings);
|
|
@@ -6492,6 +7938,147 @@ var HostServer = class {
|
|
|
6492
7938
|
this.#sendLive();
|
|
6493
7939
|
return true;
|
|
6494
7940
|
}
|
|
7941
|
+
/**
|
|
7942
|
+
* Окно ушло — вместе с ним уходят и его черновики.
|
|
7943
|
+
*
|
|
7944
|
+
* Сам текст остаётся в localStorage окна и вернётся, когда сессию откроют
|
|
7945
|
+
* снова; держать ради него процесс CLI, пока окна нет, незачем. Момент
|
|
7946
|
+
* ухода считается действием: сессия, которую только что бросили с
|
|
7947
|
+
* черновиком, получает полный срок, а не остаток.
|
|
7948
|
+
*/
|
|
7949
|
+
#forgetClient(client) {
|
|
7950
|
+
this.#clients.delete(client);
|
|
7951
|
+
const drafts = this.#drafts.get(client);
|
|
7952
|
+
if (!drafts)
|
|
7953
|
+
return;
|
|
7954
|
+
this.#drafts.delete(client);
|
|
7955
|
+
const now = Date.now();
|
|
7956
|
+
for (const id of drafts)
|
|
7957
|
+
this.#draftTouchedAt.set(id, now);
|
|
7958
|
+
}
|
|
7959
|
+
/**
|
|
7960
|
+
* Остановить живые сессии, простаивающие дольше `autoStopAfterMs`.
|
|
7961
|
+
*
|
|
7962
|
+
* Только пользовательские: боковой вопрос уходит вместе с родителем, у
|
|
7963
|
+
* сессии поиска проекта своя жизнь. Простой — это когда сессия ничего не
|
|
7964
|
+
* делает, в ней ничего не происходит и ни одно окно не держит для неё
|
|
7965
|
+
* черновик (см. `AppSettings.autoStopAfterMs`).
|
|
7966
|
+
*/
|
|
7967
|
+
async #stopIdleSessions() {
|
|
7968
|
+
const after = this.#settings.current.autoStopAfterMs;
|
|
7969
|
+
if (after <= 0)
|
|
7970
|
+
return;
|
|
7971
|
+
const now = Date.now();
|
|
7972
|
+
const drafted = /* @__PURE__ */ new Set();
|
|
7973
|
+
for (const ids of this.#drafts.values())
|
|
7974
|
+
for (const id of ids)
|
|
7975
|
+
drafted.add(id);
|
|
7976
|
+
for (const session of [...this.#sessions.values()]) {
|
|
7977
|
+
if (!session.workspaceId || session === this.#finderSession)
|
|
7978
|
+
continue;
|
|
7979
|
+
const summary = session.summary();
|
|
7980
|
+
if (summary.parentSessionId)
|
|
7981
|
+
continue;
|
|
7982
|
+
if (drafted.has(session.id))
|
|
7983
|
+
continue;
|
|
7984
|
+
if (BUSY_STATES.has(summary.state) || summary.backgroundWork)
|
|
7985
|
+
continue;
|
|
7986
|
+
const side = this.#sides.get(session.id);
|
|
7987
|
+
if (side && BUSY_STATES.has(side.state))
|
|
7988
|
+
continue;
|
|
7989
|
+
const lastActivity = Math.max(summary.updatedAt, side?.summary().updatedAt ?? 0, this.#draftTouchedAt.get(session.id) ?? 0);
|
|
7990
|
+
if (now - lastActivity < after)
|
|
7991
|
+
continue;
|
|
7992
|
+
try {
|
|
7993
|
+
await this.#dropSession(session.id, "idle");
|
|
7994
|
+
} catch (error) {
|
|
7995
|
+
console.error("[auto-stop]", error instanceof Error ? error.message : String(error));
|
|
7996
|
+
}
|
|
7997
|
+
this.#draftTouchedAt.delete(session.id);
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
// -------------------------------------------------------------------------
|
|
8001
|
+
// Остановка у лимита плана
|
|
8002
|
+
// -------------------------------------------------------------------------
|
|
8003
|
+
/** Пользовательские сессии Claude — те, что останавливаются у лимита. */
|
|
8004
|
+
#claudeUserSessions() {
|
|
8005
|
+
return [...this.#sessions.values()].filter((s) => s instanceof ClaudeSession && s !== this.#finderSession && Boolean(s.workspaceId) && !s.summary().parentSessionId);
|
|
8006
|
+
}
|
|
8007
|
+
#isBusy(session) {
|
|
8008
|
+
const summary = session.summary();
|
|
8009
|
+
return BUSY_STATES.has(summary.state) || summary.backgroundWork;
|
|
8010
|
+
}
|
|
8011
|
+
/**
|
|
8012
|
+
* Перечитать лимиты, пока хоть одна сессия занята. Сама сессия делает это
|
|
8013
|
+
* по результатам инструментов, а долгий шаг модели без инструментов иначе
|
|
8014
|
+
* прошёл бы мимо порога. Лимиты общие на аккаунт — спросить одну хватит.
|
|
8015
|
+
*/
|
|
8016
|
+
#sweepUsage() {
|
|
8017
|
+
const busy = this.#claudeUserSessions().find((s) => this.#isBusy(s));
|
|
8018
|
+
if (busy)
|
|
8019
|
+
void busy.refreshUsage();
|
|
8020
|
+
}
|
|
8021
|
+
async #checkLimit(usage) {
|
|
8022
|
+
if (this.#limitStopping)
|
|
8023
|
+
return;
|
|
8024
|
+
const hit = crossedLimit(usage, this.#settings.current, (limit) => this.#limitStop.fired(limit));
|
|
8025
|
+
if (!hit)
|
|
8026
|
+
return;
|
|
8027
|
+
this.#limitStopping = true;
|
|
8028
|
+
try {
|
|
8029
|
+
await this.#limitStop.mark(hit.limit, hit.until);
|
|
8030
|
+
await this.#stopAtLimit(hit);
|
|
8031
|
+
} catch (error) {
|
|
8032
|
+
console.error("[limit-stop]", error instanceof Error ? error.message : String(error));
|
|
8033
|
+
} finally {
|
|
8034
|
+
this.#limitStopping = false;
|
|
8035
|
+
}
|
|
8036
|
+
}
|
|
8037
|
+
/**
|
|
8038
|
+
* Остановить все живые сессии Claude: занятые — после хода хэндоффа (или с
|
|
8039
|
+
* хэндоффом из журнала, если агент не ответил), простаивающие — сразу.
|
|
8040
|
+
*/
|
|
8041
|
+
async #stopAtLimit(hit) {
|
|
8042
|
+
const sessions = this.#claudeUserSessions();
|
|
8043
|
+
if (sessions.length === 0)
|
|
8044
|
+
return;
|
|
8045
|
+
const prompt = handoffPrompt(hit);
|
|
8046
|
+
const results = await Promise.all(sessions.map(async (session) => {
|
|
8047
|
+
let saved = false;
|
|
8048
|
+
if (this.#isBusy(session)) {
|
|
8049
|
+
const text = await session.runHandoff(prompt, HANDOFF_TIMEOUT_MS);
|
|
8050
|
+
const summary = session.summary();
|
|
8051
|
+
const handoff = {
|
|
8052
|
+
sessionId: summary.claudeSessionId ?? session.id,
|
|
8053
|
+
workspaceId: session.workspaceId,
|
|
8054
|
+
title: summary.title ?? "",
|
|
8055
|
+
limit: hit.limit,
|
|
8056
|
+
utilization: hit.window.utilization,
|
|
8057
|
+
resetsAt: hit.window.resetsAt,
|
|
8058
|
+
stoppedAt: Date.now(),
|
|
8059
|
+
source: text ? "agent" : "journal",
|
|
8060
|
+
text: text ?? journalHandoff(session.journal.since(0))
|
|
8061
|
+
};
|
|
8062
|
+
try {
|
|
8063
|
+
await this.#handoffs.save(handoff);
|
|
8064
|
+
saved = true;
|
|
8065
|
+
this.#broadcast({ type: "handoff.state", sessionId: handoff.sessionId, handoff });
|
|
8066
|
+
} catch (error) {
|
|
8067
|
+
console.error("[limit-stop]", error instanceof Error ? error.message : String(error));
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
await this.#dropSession(session.id, "limit").catch(() => false);
|
|
8071
|
+
return saved;
|
|
8072
|
+
}));
|
|
8073
|
+
this.#broadcast({
|
|
8074
|
+
type: "limit.stopped",
|
|
8075
|
+
limit: hit.limit,
|
|
8076
|
+
utilization: hit.window.utilization,
|
|
8077
|
+
resetsAt: hit.window.resetsAt,
|
|
8078
|
+
stopped: results.length,
|
|
8079
|
+
handoffs: results.filter(Boolean).length
|
|
8080
|
+
});
|
|
8081
|
+
}
|
|
6495
8082
|
/**
|
|
6496
8083
|
* Живые сессии всех проектов — вкладке запущенных.
|
|
6497
8084
|
*
|
|
@@ -6546,6 +8133,26 @@ var HostServer = class {
|
|
|
6546
8133
|
const workspace = this.#workspaces.find(frame.workspaceId);
|
|
6547
8134
|
if (!workspace)
|
|
6548
8135
|
throw new Error(`\u0412\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${frame.workspaceId}`);
|
|
8136
|
+
const agyRecord = frame.resume ? await this.#antigravity.find(frame.resume) : null;
|
|
8137
|
+
if (agyRecord || !frame.resume && frame.provider === "antigravity") {
|
|
8138
|
+
if (agyRecord)
|
|
8139
|
+
await this.#archive.unarchive([agyRecord.id]);
|
|
8140
|
+
const session2 = new AntigravitySession({
|
|
8141
|
+
workspaceId: workspace.id,
|
|
8142
|
+
cwd: agyRecord?.cwd ?? workspace.path,
|
|
8143
|
+
store: this.#antigravity,
|
|
8144
|
+
...agyRecord ? { record: agyRecord } : {},
|
|
8145
|
+
...frame.permissionMode ? { permissionMode: frame.permissionMode } : {},
|
|
8146
|
+
...frame.model ? { model: frame.model } : {},
|
|
8147
|
+
defaultTitle: workspace.name
|
|
8148
|
+
});
|
|
8149
|
+
this.#wire(session2);
|
|
8150
|
+
this.#sessions.set(session2.id, session2);
|
|
8151
|
+
session2.start();
|
|
8152
|
+
this.#sendLive();
|
|
8153
|
+
this.#send(socket, { type: "session.summary", session: session2.summary() });
|
|
8154
|
+
return;
|
|
8155
|
+
}
|
|
6549
8156
|
const resumed = frame.resume && !frame.fork ? (await this.#workspaces.sessionsOnDisk(workspace.id)).find((info) => info.sessionId === frame.resume) : void 0;
|
|
6550
8157
|
const resumedTitle = resumed ? diskTitle(resumed) : null;
|
|
6551
8158
|
if (frame.resume)
|
|
@@ -6602,12 +8209,15 @@ var HostServer = class {
|
|
|
6602
8209
|
});
|
|
6603
8210
|
session.on("usage", (usage) => {
|
|
6604
8211
|
this.#broadcast({ type: "usage.state", sessionId: session.id, usage });
|
|
8212
|
+
if (session instanceof ClaudeSession)
|
|
8213
|
+
void this.#checkLimit(usage);
|
|
6605
8214
|
});
|
|
6606
8215
|
session.on("context", (context) => {
|
|
6607
8216
|
this.#broadcast({ type: "context.state", sessionId: session.id, context });
|
|
6608
8217
|
});
|
|
6609
8218
|
session.on("stderr", (data) => {
|
|
6610
|
-
|
|
8219
|
+
const tag = session instanceof AntigravitySession ? "agy" : "claude";
|
|
8220
|
+
process.stderr.write(`[${tag} ${session.id.slice(0, 8)}] ${data}`);
|
|
6611
8221
|
});
|
|
6612
8222
|
}
|
|
6613
8223
|
// -------------------------------------------------------------------------
|
|
@@ -6626,6 +8236,9 @@ var HostServer = class {
|
|
|
6626
8236
|
* разговора пока и не было.
|
|
6627
8237
|
*/
|
|
6628
8238
|
#ensureSide(parent) {
|
|
8239
|
+
if (parent instanceof AntigravitySession) {
|
|
8240
|
+
throw new Error("\u0412\u043E\u043F\u0440\u043E\u0441 \u043D\u0430 \u043F\u043E\u043B\u044F\u0445 \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0441\u0435\u0441\u0441\u0438\u044F\u0445 Claude");
|
|
8241
|
+
}
|
|
6629
8242
|
const existing = this.#sides.get(parent.id);
|
|
6630
8243
|
if (existing && existing.state !== "closed" && existing.state !== "error") {
|
|
6631
8244
|
return existing;
|
|
@@ -6923,6 +8536,11 @@ var HostServer = class {
|
|
|
6923
8536
|
live.setTitle(title);
|
|
6924
8537
|
return;
|
|
6925
8538
|
}
|
|
8539
|
+
if (await this.#antigravity.rename(sessionId, title)) {
|
|
8540
|
+
if (workspaceId)
|
|
8541
|
+
await this.#sendSessionList(workspaceId);
|
|
8542
|
+
return;
|
|
8543
|
+
}
|
|
6926
8544
|
const dir = workspaceId ? this.#workspaces.find(workspaceId)?.path : void 0;
|
|
6927
8545
|
await renameSession2(sessionId, title, dir ? { dir } : void 0);
|
|
6928
8546
|
if (!workspaceId)
|
|
@@ -6944,6 +8562,7 @@ var HostServer = class {
|
|
|
6944
8562
|
const workspacePath = this.#workspaces.find(workspaceId)?.path ?? "";
|
|
6945
8563
|
const historical = onDisk.filter((info) => !liveIds.has(info.sessionId) && info.customTitle !== SIDE_TITLE && info.customTitle !== FINDER_TITLE).map((info) => ({
|
|
6946
8564
|
id: info.sessionId,
|
|
8565
|
+
provider: "claude",
|
|
6947
8566
|
claudeSessionId: info.sessionId,
|
|
6948
8567
|
workspaceId,
|
|
6949
8568
|
cwd: info.cwd ?? workspacePath,
|
|
@@ -6968,6 +8587,11 @@ var HostServer = class {
|
|
|
6968
8587
|
lastSeq: 0,
|
|
6969
8588
|
totalCostUsd: 0
|
|
6970
8589
|
}));
|
|
8590
|
+
for (const record of await this.#antigravity.list(workspaceId)) {
|
|
8591
|
+
if (liveIds.has(record.id))
|
|
8592
|
+
continue;
|
|
8593
|
+
historical.push(antigravitySummary(record));
|
|
8594
|
+
}
|
|
6971
8595
|
const known = /* @__PURE__ */ new Set([...liveIds, ...historical.map((s) => s.id)]);
|
|
6972
8596
|
await this.#archive.sweep(workspaceId, historical.filter((s) => !this.#pins.has(s.id)).map((s) => ({ id: s.id, updatedAt: s.updatedAt })), known, this.#settings.current.autoArchiveAfterMs);
|
|
6973
8597
|
await this.#pins.sweep(workspaceId, known);
|
|
@@ -7020,8 +8644,8 @@ var HostServer = class {
|
|
|
7020
8644
|
return;
|
|
7021
8645
|
}
|
|
7022
8646
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
7023
|
-
const requested =
|
|
7024
|
-
const target = requested.startsWith(webRoot) && existsSync4(requested) && statSync3(requested).isFile() ? requested :
|
|
8647
|
+
const requested = path23.join(webRoot, path23.normalize(url2.pathname));
|
|
8648
|
+
const target = requested.startsWith(webRoot) && existsSync4(requested) && statSync3(requested).isFile() ? requested : path23.join(webRoot, "index.html");
|
|
7025
8649
|
if (!existsSync4(target)) {
|
|
7026
8650
|
res.writeHead(404).end("not found");
|
|
7027
8651
|
return;
|
|
@@ -7047,12 +8671,36 @@ function contentType(file) {
|
|
|
7047
8671
|
".svg": "image/svg+xml",
|
|
7048
8672
|
".woff2": "font/woff2"
|
|
7049
8673
|
};
|
|
7050
|
-
return types[
|
|
8674
|
+
return types[path23.extname(file)] ?? "application/octet-stream";
|
|
8675
|
+
}
|
|
8676
|
+
function antigravitySummary(record) {
|
|
8677
|
+
return {
|
|
8678
|
+
id: record.id,
|
|
8679
|
+
provider: "antigravity",
|
|
8680
|
+
claudeSessionId: record.id,
|
|
8681
|
+
workspaceId: record.workspaceId,
|
|
8682
|
+
cwd: record.cwd,
|
|
8683
|
+
parentSessionId: null,
|
|
8684
|
+
title: record.title,
|
|
8685
|
+
state: "closed",
|
|
8686
|
+
lastTurn: null,
|
|
8687
|
+
backgroundWork: false,
|
|
8688
|
+
model: record.model,
|
|
8689
|
+
permissionMode: record.permissionMode,
|
|
8690
|
+
effort: record.effort,
|
|
8691
|
+
ultracode: false,
|
|
8692
|
+
thinking: true,
|
|
8693
|
+
switchModelsOnFlag: true,
|
|
8694
|
+
createdAt: record.createdAt,
|
|
8695
|
+
updatedAt: record.updatedAt,
|
|
8696
|
+
lastSeq: 0,
|
|
8697
|
+
totalCostUsd: 0
|
|
8698
|
+
};
|
|
7051
8699
|
}
|
|
7052
8700
|
|
|
7053
8701
|
// dist/ccv.js
|
|
7054
|
-
var HERE =
|
|
7055
|
-
var PACKAGE_ROOT =
|
|
8702
|
+
var HERE = path24.dirname(fileURLToPath(import.meta.url));
|
|
8703
|
+
var PACKAGE_ROOT = path24.resolve(HERE, "..");
|
|
7056
8704
|
var USAGE = `ccv \u2014 Claude Code Visual: \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u0445\u043E\u0441\u0442 Claude Code \u0441 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043E\u043C \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435.
|
|
7057
8705
|
|
|
7058
8706
|
\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
|
|
@@ -7119,7 +8767,7 @@ var server = new HostServer({
|
|
|
7119
8767
|
dataDir: defaults.dataDir,
|
|
7120
8768
|
// UI лежит в пакете рядом с бандлом — это и есть разница между установленной
|
|
7121
8769
|
// утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
|
|
7122
|
-
webRoot: process.env.CCV_WEB_ROOT ??
|
|
8770
|
+
webRoot: process.env.CCV_WEB_ROOT ?? path24.join(PACKAGE_ROOT, "web"),
|
|
7123
8771
|
version,
|
|
7124
8772
|
relayUrl: defaults.relayUrl
|
|
7125
8773
|
});
|
|
@@ -7148,7 +8796,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
7148
8796
|
}
|
|
7149
8797
|
async function packageVersion() {
|
|
7150
8798
|
try {
|
|
7151
|
-
const text = await
|
|
8799
|
+
const text = await readFile17(path24.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
7152
8800
|
return JSON.parse(text).version ?? "0.0.0";
|
|
7153
8801
|
} catch {
|
|
7154
8802
|
return "0.0.0";
|
|
@@ -7192,7 +8840,7 @@ function openBrowser(target) {
|
|
|
7192
8840
|
return;
|
|
7193
8841
|
}
|
|
7194
8842
|
const command = process.platform === "win32" ? { file: "cmd", args: ["/c", "start", "", target] } : process.platform === "darwin" ? { file: "open", args: [target] } : { file: "xdg-open", args: [target] };
|
|
7195
|
-
const child =
|
|
8843
|
+
const child = spawn3(command.file, command.args, {
|
|
7196
8844
|
stdio: "ignore",
|
|
7197
8845
|
detached: true,
|
|
7198
8846
|
windowsHide: true
|