@i14ks/ccv 0.14.0 → 0.15.3
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 +1856 -191
- package/package.json +1 -1
- package/web/assets/antigravity-logo-DRKQga2U.png +0 -0
- package/web/assets/{codemirror-DfnUEAQ1.js → codemirror-Dg4vvH8i.js} +1 -1
- package/web/assets/{index-DLoVg97c.js → index-0iOvZWuA.js} +1 -1
- package/web/assets/{index-BUVVNNln.js → index-B96oAtZO.js} +1 -1
- package/web/assets/{index-6WrTm4Cl.js → index-Bg6okyOc.js} +1 -1
- package/web/assets/index-Bo84aMH8.js +134 -0
- package/web/assets/{index-BSHub4Y3.js → index-Bpw3TKRV.js} +1 -1
- package/web/assets/{index-Aa3M3Mcp.js → index-CB6_9alP.js} +1 -1
- package/web/assets/{index-DNvUBsh8.js → index-CM3XoNnk.js} +1 -1
- package/web/assets/{index-UUIF3jgc.js → index-CNi4N4PL.js} +1 -1
- package/web/assets/index-COiPHc3p.css +1 -0
- package/web/assets/{index-C_0hLH-6.js → index-CRyJJS40.js} +1 -1
- package/web/assets/{index-C33PhVuR.js → index-CTrrHYKx.js} +1 -1
- package/web/assets/{index-Cu9yVjDd.js → index-CanbdA5V.js} +1 -1
- package/web/assets/{index-BFyhFly9.js → index-CrC2iAsw.js} +1 -1
- package/web/assets/{index-B4QS6XYB.js → index-CzbpsO2-.js} +1 -1
- package/web/assets/{index-DdWyOJPD.js → index-D04xl5g2.js} +1 -1
- package/web/assets/{index-CE-6gz9W.js → index-DB0qYzJa.js} +1 -1
- package/web/assets/{index-BrhFysJH.js → index-DMpA08Ie.js} +1 -1
- package/web/assets/{index-BBJ7uyxo.js → index-LHO2W5bU.js} +1 -1
- package/web/assets/{index-DdCEnSCp.js → index-SxQ8qp4S.js} +1 -1
- package/web/assets/{index-CReKaug6.js → index-ZEjcSNDk.js} +1 -1
- package/web/assets/{index-1NZdgXKt.js → index-kBFqQTK1.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-i3oOZBX4.js +0 -133
- package/web/assets/index-ihyDKGfF.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,54 @@ 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
|
+
if (!turn && !this.#closing && this.#state !== "idle") {
|
|
1807
|
+
const settled = this.#nextTurnEnd(INTERRUPT_SETTLE_MS);
|
|
1808
|
+
await this.interrupt().catch(() => void 0);
|
|
1809
|
+
await settled;
|
|
1810
|
+
}
|
|
1811
|
+
const text = turn?.text?.trim();
|
|
1812
|
+
return turn && !turn.isError && text ? text : null;
|
|
1813
|
+
} catch {
|
|
1814
|
+
return null;
|
|
1815
|
+
} finally {
|
|
1816
|
+
this.#handoffMode = false;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
/** Ближайший конец хода (`result`) или `null`, если не дождались. */
|
|
1820
|
+
#nextTurnEnd(timeoutMs) {
|
|
1821
|
+
return new Promise((resolve) => {
|
|
1822
|
+
const onEnd = (turn) => {
|
|
1823
|
+
clearTimeout(timer);
|
|
1824
|
+
resolve(turn);
|
|
1825
|
+
};
|
|
1826
|
+
const timer = setTimeout(() => {
|
|
1827
|
+
this.off("turnEnd", onEnd);
|
|
1828
|
+
resolve(null);
|
|
1829
|
+
}, timeoutMs);
|
|
1830
|
+
this.once("turnEnd", onEnd);
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1776
1833
|
/**
|
|
1777
1834
|
* Смена режима прав на лету.
|
|
1778
1835
|
*
|
|
@@ -1867,7 +1924,6 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1867
1924
|
const raw = await q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET();
|
|
1868
1925
|
this.#usage = toUsageState(raw);
|
|
1869
1926
|
this.emit("usage", this.#usage);
|
|
1870
|
-
writeFinisherSnapshot(this.#usage, this.#claudeSessionId, this.cwd);
|
|
1871
1927
|
} catch {
|
|
1872
1928
|
} finally {
|
|
1873
1929
|
this.#usageAt = Date.now();
|
|
@@ -2036,6 +2092,18 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2036
2092
|
}
|
|
2037
2093
|
};
|
|
2038
2094
|
};
|
|
2095
|
+
#guardHandoff() {
|
|
2096
|
+
if (!this.#handoffMode)
|
|
2097
|
+
return { continue: true };
|
|
2098
|
+
return {
|
|
2099
|
+
continue: true,
|
|
2100
|
+
hookSpecificOutput: {
|
|
2101
|
+
hookEventName: "PreToolUse",
|
|
2102
|
+
permissionDecision: "deny",
|
|
2103
|
+
permissionDecisionReason: HANDOFF_TOOL_DENIAL
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2039
2107
|
/**
|
|
2040
2108
|
* Мост между `canUseTool` из SDK и решением человека в UI.
|
|
2041
2109
|
*
|
|
@@ -2053,6 +2121,9 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2053
2121
|
* это и происходило, пока вопросы показывались обычной карточкой прав.
|
|
2054
2122
|
*/
|
|
2055
2123
|
#canUseTool = (toolName, input, { signal, decisionReason, toolUseID }) => {
|
|
2124
|
+
if (this.#handoffMode) {
|
|
2125
|
+
return Promise.resolve({ behavior: "deny", message: HANDOFF_TOOL_DENIAL });
|
|
2126
|
+
}
|
|
2056
2127
|
if (this.#options.readOnly) {
|
|
2057
2128
|
this.#record({
|
|
2058
2129
|
kind: "error",
|
|
@@ -2202,6 +2273,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2202
2273
|
contextTokens: this.#contextTokens,
|
|
2203
2274
|
text: "result" in message ? message.result : null
|
|
2204
2275
|
});
|
|
2276
|
+
this.emit("turnEnd", {
|
|
2277
|
+
text: "result" in message ? message.result : null,
|
|
2278
|
+
isError: message.is_error
|
|
2279
|
+
});
|
|
2205
2280
|
if (this.#options.outputFormat) {
|
|
2206
2281
|
this.emit("structured", {
|
|
2207
2282
|
data: "structured_output" in message ? message.structured_output : void 0,
|
|
@@ -2427,27 +2502,6 @@ function toUsageState(raw) {
|
|
|
2427
2502
|
fetchedAt: Date.now()
|
|
2428
2503
|
};
|
|
2429
2504
|
}
|
|
2430
|
-
function writeFinisherSnapshot(usage, sessionId, cwd) {
|
|
2431
|
-
if (!usage.available)
|
|
2432
|
-
return;
|
|
2433
|
-
const dir = path5.join(homedir3(), ".claude", "finisher");
|
|
2434
|
-
if (!existsSync(dir))
|
|
2435
|
-
return;
|
|
2436
|
-
const snapshot = {
|
|
2437
|
-
updated_at: Date.now(),
|
|
2438
|
-
session_id: sessionId,
|
|
2439
|
-
cwd,
|
|
2440
|
-
rate_limits: {
|
|
2441
|
-
five_hour: usage.fiveHour?.utilization ?? null,
|
|
2442
|
-
seven_day: usage.sevenDay?.utilization ?? null,
|
|
2443
|
-
spend_limit: null
|
|
2444
|
-
}
|
|
2445
|
-
};
|
|
2446
|
-
try {
|
|
2447
|
-
writeFileSync(path5.join(dir, "usage-cache.json"), JSON.stringify(snapshot, null, 2));
|
|
2448
|
-
} catch {
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
2505
|
function toUsageWindow(raw) {
|
|
2452
2506
|
if (!raw || typeof raw.utilization !== "number")
|
|
2453
2507
|
return null;
|
|
@@ -2458,29 +2512,1168 @@ function toUsageWindow(raw) {
|
|
|
2458
2512
|
resetsAt: typeof raw.resets_at === "string" ? raw.resets_at : ""
|
|
2459
2513
|
};
|
|
2460
2514
|
}
|
|
2461
|
-
function contentToText2(content) {
|
|
2462
|
-
if (typeof content === "string")
|
|
2463
|
-
return content;
|
|
2464
|
-
if (!Array.isArray(content))
|
|
2465
|
-
return "";
|
|
2466
|
-
return content.map((block) => {
|
|
2467
|
-
if (typeof block === "string")
|
|
2468
|
-
return block;
|
|
2469
|
-
if (block && typeof block === "object" && "text" in block) {
|
|
2470
|
-
return String(block.text);
|
|
2515
|
+
function contentToText2(content) {
|
|
2516
|
+
if (typeof content === "string")
|
|
2517
|
+
return content;
|
|
2518
|
+
if (!Array.isArray(content))
|
|
2519
|
+
return "";
|
|
2520
|
+
return content.map((block) => {
|
|
2521
|
+
if (typeof block === "string")
|
|
2522
|
+
return block;
|
|
2523
|
+
if (block && typeof block === "object" && "text" in block) {
|
|
2524
|
+
return String(block.text);
|
|
2525
|
+
}
|
|
2526
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
2527
|
+
return `[${String(block.type)}]`;
|
|
2528
|
+
}
|
|
2529
|
+
return "";
|
|
2530
|
+
}).join("");
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// ../host/dist/antigravity-session.js
|
|
2534
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
2535
|
+
import { spawn as spawn2, execFile as execFile3 } from "node:child_process";
|
|
2536
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2537
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2538
|
+
import { tmpdir } from "node:os";
|
|
2539
|
+
import path7 from "node:path";
|
|
2540
|
+
import { createInterface } from "node:readline";
|
|
2541
|
+
|
|
2542
|
+
// ../host/dist/antigravity-cli.js
|
|
2543
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2544
|
+
import { existsSync } from "node:fs";
|
|
2545
|
+
import { homedir as homedir3 } from "node:os";
|
|
2546
|
+
import path6 from "node:path";
|
|
2547
|
+
function agyExecutable() {
|
|
2548
|
+
const forced = process.env.CCV_AGY_PATH;
|
|
2549
|
+
if (forced && existsSync(forced))
|
|
2550
|
+
return forced;
|
|
2551
|
+
const names2 = process.platform === "win32" ? ["agy.exe", "agy"] : ["agy"];
|
|
2552
|
+
const dirs = (process.env.PATH ?? "").split(path6.delimiter).filter(Boolean);
|
|
2553
|
+
if (process.platform === "win32") {
|
|
2554
|
+
const local = process.env.LOCALAPPDATA ?? path6.join(homedir3(), "AppData", "Local");
|
|
2555
|
+
dirs.push(path6.join(local, "agy", "bin"));
|
|
2556
|
+
} else {
|
|
2557
|
+
dirs.push(path6.join(homedir3(), ".local", "bin"), "/usr/local/bin", "/opt/homebrew/bin");
|
|
2558
|
+
}
|
|
2559
|
+
for (const dir of dirs) {
|
|
2560
|
+
for (const name of names2) {
|
|
2561
|
+
const candidate = path6.join(dir, name);
|
|
2562
|
+
if (existsSync(candidate))
|
|
2563
|
+
return candidate;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2568
|
+
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.";
|
|
2569
|
+
var modelsCache = null;
|
|
2570
|
+
function agyModels(file) {
|
|
2571
|
+
if (modelsCache)
|
|
2572
|
+
return modelsCache;
|
|
2573
|
+
const pending = new Promise((resolve) => {
|
|
2574
|
+
execFile2(file, ["models"], { timeout: 3e4, windowsHide: true }, (error, stdout) => {
|
|
2575
|
+
if (error) {
|
|
2576
|
+
resolve([]);
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
const models = [];
|
|
2580
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
2581
|
+
const [value, displayName] = line.split(" ");
|
|
2582
|
+
if (!value || !displayName || /\s/.test(value))
|
|
2583
|
+
continue;
|
|
2584
|
+
models.push({
|
|
2585
|
+
value,
|
|
2586
|
+
resolvedModel: value,
|
|
2587
|
+
displayName: displayName.trim(),
|
|
2588
|
+
description: "",
|
|
2589
|
+
supportsEffort: false,
|
|
2590
|
+
effortLevels: [],
|
|
2591
|
+
supportsThinking: false,
|
|
2592
|
+
supportsAutoMode: false
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
resolve(models);
|
|
2596
|
+
});
|
|
2597
|
+
});
|
|
2598
|
+
modelsCache = pending;
|
|
2599
|
+
void pending.then((models) => {
|
|
2600
|
+
if (models.length === 0)
|
|
2601
|
+
modelsCache = null;
|
|
2602
|
+
});
|
|
2603
|
+
return pending;
|
|
2604
|
+
}
|
|
2605
|
+
function agyCommand(file, command, cwd) {
|
|
2606
|
+
return new Promise((resolve) => {
|
|
2607
|
+
execFile2(file, ["--output-format", "json", "-p", command], {
|
|
2608
|
+
cwd,
|
|
2609
|
+
timeout: 6e4,
|
|
2610
|
+
windowsHide: true,
|
|
2611
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
2612
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
2613
|
+
}, (error, stdout) => {
|
|
2614
|
+
const line = stdout.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.startsWith("{")).pop();
|
|
2615
|
+
if (line) {
|
|
2616
|
+
try {
|
|
2617
|
+
resolve(JSON.parse(line));
|
|
2618
|
+
return;
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
resolve({ status: "ERROR", error: error ? error.message : "agy \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B" });
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
var USAGE_TTL_MS = 3e4;
|
|
2627
|
+
var usageCache = null;
|
|
2628
|
+
var usageInFlight = null;
|
|
2629
|
+
var usageError = null;
|
|
2630
|
+
function agyUsageError() {
|
|
2631
|
+
return usageError;
|
|
2632
|
+
}
|
|
2633
|
+
function agyUsage(file, force = false) {
|
|
2634
|
+
if (!force && usageCache && Date.now() - usageCache.at < USAGE_TTL_MS) {
|
|
2635
|
+
return Promise.resolve(usageCache.value);
|
|
2636
|
+
}
|
|
2637
|
+
if (usageInFlight)
|
|
2638
|
+
return usageInFlight;
|
|
2639
|
+
usageInFlight = agyCommand(file, "/usage").then((reply) => {
|
|
2640
|
+
const value = parseUsage(reply);
|
|
2641
|
+
if (value)
|
|
2642
|
+
usageCache = { at: Date.now(), value };
|
|
2643
|
+
usageError = value ? null : reply.error || "agy \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u043A\u0432\u043E\u0442\u0443";
|
|
2644
|
+
return value;
|
|
2645
|
+
}).finally(() => {
|
|
2646
|
+
usageInFlight = null;
|
|
2647
|
+
});
|
|
2648
|
+
return usageInFlight;
|
|
2649
|
+
}
|
|
2650
|
+
var PERIOD_LABELS = {
|
|
2651
|
+
weekly: "\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442",
|
|
2652
|
+
daily: "\u0434\u043D\u0435\u0432\u043D\u043E\u0439 \u043B\u0438\u043C\u0438\u0442",
|
|
2653
|
+
monthly: "\u043C\u0435\u0441\u044F\u0447\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442"
|
|
2654
|
+
};
|
|
2655
|
+
function parseUsage(reply) {
|
|
2656
|
+
if (reply.status !== "SUCCESS")
|
|
2657
|
+
return null;
|
|
2658
|
+
const data = reply.command?.data;
|
|
2659
|
+
if (!data || !Array.isArray(data.groups))
|
|
2660
|
+
return null;
|
|
2661
|
+
const buckets = [];
|
|
2662
|
+
for (const group of data.groups) {
|
|
2663
|
+
const groupName = group.name ?? "";
|
|
2664
|
+
for (const bucket of group.buckets ?? []) {
|
|
2665
|
+
if (typeof bucket.remaining_fraction !== "number")
|
|
2666
|
+
continue;
|
|
2667
|
+
const used = Math.min(100, Math.max(0, (1 - bucket.remaining_fraction) * 100));
|
|
2668
|
+
buckets.push({
|
|
2669
|
+
id: bucket.id ?? `${groupName}:${bucket.name ?? ""}`,
|
|
2670
|
+
label: shortGroupLabel(groupName),
|
|
2671
|
+
// Имя окна у `agy` — «Weekly Limit Remaining», а полоса показывает
|
|
2672
|
+
// израсходованное: подпись с «Remaining» читалась бы наоборот.
|
|
2673
|
+
title: [groupName, PERIOD_LABELS[bucket.window ?? ""] ?? bucket.name].filter(Boolean).join(" \u2014 "),
|
|
2674
|
+
description: group.description ?? "",
|
|
2675
|
+
period: bucket.window ?? "",
|
|
2676
|
+
window: { utilization: used, resetsAt: bucket.reset_time ?? "" }
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return {
|
|
2681
|
+
available: buckets.length > 0,
|
|
2682
|
+
plan: "",
|
|
2683
|
+
fiveHour: null,
|
|
2684
|
+
sevenDay: null,
|
|
2685
|
+
sevenDayOpus: null,
|
|
2686
|
+
buckets,
|
|
2687
|
+
fetchedAt: Date.now()
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
function shortGroupLabel(name) {
|
|
2691
|
+
const short = name.replace(/\s+models?$/i, "").replace(/\s+and\s+/gi, "/").trim();
|
|
2692
|
+
return short || name;
|
|
2693
|
+
}
|
|
2694
|
+
var INTERACTIVE_COMMANDS = /* @__PURE__ */ new Set(["config", "hooks", "permissions", "context"]);
|
|
2695
|
+
var AGY_CLI_COMMANDS = /* @__PURE__ */ new Set([
|
|
2696
|
+
"help",
|
|
2697
|
+
"usage",
|
|
2698
|
+
"quota",
|
|
2699
|
+
"skills",
|
|
2700
|
+
"agents",
|
|
2701
|
+
"changelog",
|
|
2702
|
+
"credits",
|
|
2703
|
+
"model",
|
|
2704
|
+
"effort"
|
|
2705
|
+
]);
|
|
2706
|
+
var COMMAND_ARGS = { model: "[\u043C\u043E\u0434\u0435\u043B\u044C]", effort: "[low|medium|high|max]" };
|
|
2707
|
+
var catalogCache = null;
|
|
2708
|
+
function agyCommandCatalog(file, cwd) {
|
|
2709
|
+
if (catalogCache)
|
|
2710
|
+
return catalogCache;
|
|
2711
|
+
const pending = Promise.all([agyCommand(file, "/help", cwd), agyCommand(file, "/skills", cwd)]).then(([help, skills]) => {
|
|
2712
|
+
const commands = [];
|
|
2713
|
+
const helpData = help.command?.data;
|
|
2714
|
+
for (const c of helpData?.commands ?? []) {
|
|
2715
|
+
if (!c.name || INTERACTIVE_COMMANDS.has(c.name))
|
|
2716
|
+
continue;
|
|
2717
|
+
commands.push({
|
|
2718
|
+
name: c.name,
|
|
2719
|
+
description: c.description ?? "",
|
|
2720
|
+
argumentHint: COMMAND_ARGS[c.name] ?? "",
|
|
2721
|
+
aliases: c.aliases ?? []
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
const skillsData = skills.command?.data;
|
|
2725
|
+
for (const s of skillsData?.skills ?? []) {
|
|
2726
|
+
if (!s.name || commands.some((c) => c.name === s.name))
|
|
2727
|
+
continue;
|
|
2728
|
+
commands.push({ name: s.name, description: s.description ?? "", argumentHint: "", aliases: [] });
|
|
2729
|
+
}
|
|
2730
|
+
return commands;
|
|
2731
|
+
});
|
|
2732
|
+
catalogCache = pending;
|
|
2733
|
+
void pending.then((commands) => {
|
|
2734
|
+
if (commands.length === 0)
|
|
2735
|
+
catalogCache = null;
|
|
2736
|
+
});
|
|
2737
|
+
return pending;
|
|
2738
|
+
}
|
|
2739
|
+
function agyContextWindow(model) {
|
|
2740
|
+
if (!model)
|
|
2741
|
+
return null;
|
|
2742
|
+
const id = model.toLowerCase();
|
|
2743
|
+
if (id.startsWith("gemini-"))
|
|
2744
|
+
return 1048576;
|
|
2745
|
+
if (id.startsWith("claude-"))
|
|
2746
|
+
return 2e5;
|
|
2747
|
+
if (id.startsWith("gpt-oss"))
|
|
2748
|
+
return 131072;
|
|
2749
|
+
return null;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// ../host/dist/antigravity-session.js
|
|
2753
|
+
var TRANSIENT = /* @__PURE__ */ new Set(["assistant.delta", "thinking.delta", "state"]);
|
|
2754
|
+
var AntigravitySession = class extends EventEmitter2 {
|
|
2755
|
+
id;
|
|
2756
|
+
workspaceId;
|
|
2757
|
+
cwd;
|
|
2758
|
+
journal = new Journal();
|
|
2759
|
+
provider = "antigravity";
|
|
2760
|
+
#store;
|
|
2761
|
+
#record;
|
|
2762
|
+
#history = Promise.resolve([]);
|
|
2763
|
+
#child = null;
|
|
2764
|
+
#executable = null;
|
|
2765
|
+
#state = "starting";
|
|
2766
|
+
#lastTurn = null;
|
|
2767
|
+
#capabilities = null;
|
|
2768
|
+
#titleIsDefault = false;
|
|
2769
|
+
#closing = false;
|
|
2770
|
+
/** Ходы, отправленные процессу и ещё не закрытые его `result`. */
|
|
2771
|
+
#pendingTurns = 0;
|
|
2772
|
+
/** Перезапуск (смена модели/режима), отложенный до конца хода. */
|
|
2773
|
+
#restartPending = false;
|
|
2774
|
+
#interrupting = false;
|
|
2775
|
+
/** Накопленный текст шагов-ответов: на `DONE` уходит одним `assistant.text`. */
|
|
2776
|
+
#texts = /* @__PURE__ */ new Map();
|
|
2777
|
+
#thinking = /* @__PURE__ */ new Map();
|
|
2778
|
+
/** Шаги-инструменты, о которых уже сказано `tool.use`. */
|
|
2779
|
+
#tools = /* @__PURE__ */ new Set();
|
|
2780
|
+
#contextTokens = null;
|
|
2781
|
+
/** Модель, на которой `agy` реально работает (`init.model`) — с учётом его умолчания. */
|
|
2782
|
+
#activeModel = null;
|
|
2783
|
+
#usage = null;
|
|
2784
|
+
#context = null;
|
|
2785
|
+
#commands = [];
|
|
2786
|
+
#stderrTail = "";
|
|
2787
|
+
#unsaved = [];
|
|
2788
|
+
#flushTimer = null;
|
|
2789
|
+
#updatedAt = Date.now();
|
|
2790
|
+
constructor(options2) {
|
|
2791
|
+
super();
|
|
2792
|
+
this.#store = options2.store;
|
|
2793
|
+
this.workspaceId = options2.workspaceId;
|
|
2794
|
+
this.cwd = options2.cwd;
|
|
2795
|
+
const now = Date.now();
|
|
2796
|
+
this.#record = options2.record ? { ...options2.record } : {
|
|
2797
|
+
id: randomUUID2(),
|
|
2798
|
+
conversationId: null,
|
|
2799
|
+
workspaceId: options2.workspaceId,
|
|
2800
|
+
cwd: options2.cwd,
|
|
2801
|
+
title: options2.defaultTitle ?? null,
|
|
2802
|
+
model: options2.model ?? null,
|
|
2803
|
+
permissionMode: options2.permissionMode ?? "default",
|
|
2804
|
+
effort: null,
|
|
2805
|
+
createdAt: now,
|
|
2806
|
+
updatedAt: now
|
|
2807
|
+
};
|
|
2808
|
+
if (!options2.record)
|
|
2809
|
+
this.#titleIsDefault = options2.defaultTitle !== void 0;
|
|
2810
|
+
if (options2.record && options2.permissionMode) {
|
|
2811
|
+
this.#record.permissionMode = options2.permissionMode;
|
|
2812
|
+
}
|
|
2813
|
+
this.id = this.#record.id;
|
|
2814
|
+
this.#updatedAt = this.#record.updatedAt;
|
|
2815
|
+
}
|
|
2816
|
+
// -------------------------------------------------------------------------
|
|
2817
|
+
// Публичное состояние — та же форма, что у ClaudeSession
|
|
2818
|
+
// -------------------------------------------------------------------------
|
|
2819
|
+
get state() {
|
|
2820
|
+
return this.#state;
|
|
2821
|
+
}
|
|
2822
|
+
get capabilities() {
|
|
2823
|
+
return this.#capabilities;
|
|
2824
|
+
}
|
|
2825
|
+
/** Постоянный id сессии: под ним она в архиве, закреплённых и папках. */
|
|
2826
|
+
get claudeSessionId() {
|
|
2827
|
+
return this.id;
|
|
2828
|
+
}
|
|
2829
|
+
get parentSessionId() {
|
|
2830
|
+
return null;
|
|
2831
|
+
}
|
|
2832
|
+
get usage() {
|
|
2833
|
+
return this.#usage;
|
|
2834
|
+
}
|
|
2835
|
+
get context() {
|
|
2836
|
+
return this.#context;
|
|
2837
|
+
}
|
|
2838
|
+
history() {
|
|
2839
|
+
return this.#history;
|
|
2840
|
+
}
|
|
2841
|
+
summary() {
|
|
2842
|
+
return {
|
|
2843
|
+
id: this.id,
|
|
2844
|
+
provider: "antigravity",
|
|
2845
|
+
claudeSessionId: this.id,
|
|
2846
|
+
workspaceId: this.workspaceId,
|
|
2847
|
+
cwd: this.cwd,
|
|
2848
|
+
parentSessionId: null,
|
|
2849
|
+
title: this.#record.title,
|
|
2850
|
+
state: this.#state,
|
|
2851
|
+
lastTurn: this.#lastTurn,
|
|
2852
|
+
backgroundWork: false,
|
|
2853
|
+
model: this.#record.model,
|
|
2854
|
+
permissionMode: this.#record.permissionMode,
|
|
2855
|
+
effort: this.#record.effort,
|
|
2856
|
+
ultracode: false,
|
|
2857
|
+
thinking: true,
|
|
2858
|
+
switchModelsOnFlag: true,
|
|
2859
|
+
createdAt: this.#record.createdAt,
|
|
2860
|
+
updatedAt: this.#updatedAt,
|
|
2861
|
+
lastSeq: this.journal.lastSeq,
|
|
2862
|
+
totalCostUsd: 0
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
// -------------------------------------------------------------------------
|
|
2866
|
+
// Жизненный цикл
|
|
2867
|
+
// -------------------------------------------------------------------------
|
|
2868
|
+
start() {
|
|
2869
|
+
if (this.#record.conversationId) {
|
|
2870
|
+
this.#history = this.#store.readEvents(this.id);
|
|
2871
|
+
void this.#history.then((entries) => {
|
|
2872
|
+
if (this.#contextTokens !== null)
|
|
2873
|
+
return;
|
|
2874
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
2875
|
+
const event = entries[i].event;
|
|
2876
|
+
if (event.kind === "result" && event.contextTokens !== null) {
|
|
2877
|
+
this.#contextTokens = event.contextTokens;
|
|
2878
|
+
this.#emitContext();
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
void this.#store.save(this.#record);
|
|
2885
|
+
this.#executable = agyExecutable();
|
|
2886
|
+
if (!this.#executable) {
|
|
2887
|
+
this.#record_({ kind: "error", message: AGY_MISSING_HINT });
|
|
2888
|
+
this.#setState("error");
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
this.#capabilities = this.#baseCapabilities();
|
|
2892
|
+
this.emit("capabilities");
|
|
2893
|
+
void agyModels(this.#executable).then((models) => {
|
|
2894
|
+
if (!this.#capabilities || models.length === 0)
|
|
2895
|
+
return;
|
|
2896
|
+
this.#capabilities = { ...this.#capabilities, models };
|
|
2897
|
+
this.emit("capabilities");
|
|
2898
|
+
});
|
|
2899
|
+
void agyCommandCatalog(this.#executable, this.cwd).then((commands) => {
|
|
2900
|
+
if (!this.#capabilities || commands.length === 0)
|
|
2901
|
+
return;
|
|
2902
|
+
this.#commands = commands;
|
|
2903
|
+
this.#capabilities = this.#baseCapabilities();
|
|
2904
|
+
this.emit("capabilities");
|
|
2905
|
+
});
|
|
2906
|
+
void this.refreshUsage();
|
|
2907
|
+
this.#spawn();
|
|
2908
|
+
}
|
|
2909
|
+
#baseCapabilities() {
|
|
2910
|
+
return {
|
|
2911
|
+
cwd: this.cwd,
|
|
2912
|
+
model: this.#record.model ?? this.#activeModel ?? "",
|
|
2913
|
+
permissionMode: this.#record.permissionMode,
|
|
2914
|
+
slashCommands: this.#commands.map((c) => c.name),
|
|
2915
|
+
commands: this.#commands,
|
|
2916
|
+
models: this.#capabilities?.models ?? [],
|
|
2917
|
+
tools: this.#capabilities?.tools ?? [],
|
|
2918
|
+
agents: [],
|
|
2919
|
+
mcpServers: [],
|
|
2920
|
+
flags: []
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
#args() {
|
|
2924
|
+
const args = ["--input-format", "stream-json", "--output-format", "stream-json"];
|
|
2925
|
+
switch (this.#record.permissionMode) {
|
|
2926
|
+
case "acceptEdits":
|
|
2927
|
+
args.push("--mode", "accept-edits");
|
|
2928
|
+
break;
|
|
2929
|
+
case "plan":
|
|
2930
|
+
args.push("--mode", "plan");
|
|
2931
|
+
break;
|
|
2932
|
+
case "bypassPermissions":
|
|
2933
|
+
args.push("--dangerously-skip-permissions");
|
|
2934
|
+
break;
|
|
2935
|
+
default:
|
|
2936
|
+
break;
|
|
2937
|
+
}
|
|
2938
|
+
if (this.#record.model)
|
|
2939
|
+
args.push("--model", this.#record.model);
|
|
2940
|
+
const effort = agyEffort(this.#record.effort);
|
|
2941
|
+
if (effort)
|
|
2942
|
+
args.push("--effort", effort);
|
|
2943
|
+
if (this.#record.conversationId)
|
|
2944
|
+
args.push("--conversation", this.#record.conversationId);
|
|
2945
|
+
args.push("-p=");
|
|
2946
|
+
return args;
|
|
2947
|
+
}
|
|
2948
|
+
#spawn() {
|
|
2949
|
+
if (!this.#executable || this.#closing)
|
|
2950
|
+
return;
|
|
2951
|
+
let child;
|
|
2952
|
+
try {
|
|
2953
|
+
child = spawn2(this.#executable, this.#args(), {
|
|
2954
|
+
cwd: this.cwd,
|
|
2955
|
+
windowsHide: true,
|
|
2956
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
2957
|
+
});
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
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)}` });
|
|
2960
|
+
this.#setState("error");
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
this.#child = child;
|
|
2964
|
+
this.#stderrTail = "";
|
|
2965
|
+
this.#texts.clear();
|
|
2966
|
+
this.#thinking.clear();
|
|
2967
|
+
this.#tools.clear();
|
|
2968
|
+
const lines = createInterface({ input: child.stdout });
|
|
2969
|
+
lines.on("line", (line) => {
|
|
2970
|
+
if (this.#child !== child)
|
|
2971
|
+
return;
|
|
2972
|
+
this.#handleLine(line);
|
|
2973
|
+
});
|
|
2974
|
+
child.stderr.setEncoding("utf8");
|
|
2975
|
+
child.stderr.on("data", (data) => {
|
|
2976
|
+
this.#stderrTail = (this.#stderrTail + data).slice(-4e3);
|
|
2977
|
+
this.emit("stderr", data);
|
|
2978
|
+
});
|
|
2979
|
+
child.stdin.on("error", () => void 0);
|
|
2980
|
+
child.on("error", (error) => {
|
|
2981
|
+
if (this.#child !== child)
|
|
2982
|
+
return;
|
|
2983
|
+
this.#record_({ kind: "error", message: `agy: ${describe2(error)}` });
|
|
2984
|
+
});
|
|
2985
|
+
child.on("exit", (code) => {
|
|
2986
|
+
if (this.#child !== child)
|
|
2987
|
+
return;
|
|
2988
|
+
this.#child = null;
|
|
2989
|
+
this.#onExit(code);
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
/** Процесс ушёл сам — не по нашей просьбе. */
|
|
2993
|
+
#onExit(code) {
|
|
2994
|
+
if (this.#closing)
|
|
2995
|
+
return;
|
|
2996
|
+
const busy = this.#pendingTurns > 0;
|
|
2997
|
+
this.#pendingTurns = 0;
|
|
2998
|
+
if (code === 0 && !busy) {
|
|
2999
|
+
this.#setState("idle");
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
const tail = this.#stderrTail.trim().split(/\r?\n/).slice(-3).join("\n");
|
|
3003
|
+
this.#lastTurn = busy ? "failed" : this.#lastTurn;
|
|
3004
|
+
this.#record_({
|
|
3005
|
+
kind: "error",
|
|
3006
|
+
message: `Antigravity CLI \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F (\u043A\u043E\u0434 ${code ?? "\u2014"})${tail ? `: ${tail}` : ""}`
|
|
3007
|
+
});
|
|
3008
|
+
this.#setState("error");
|
|
3009
|
+
}
|
|
3010
|
+
async close(_reason = "closed by client") {
|
|
3011
|
+
this.#closing = true;
|
|
3012
|
+
this.#kill();
|
|
3013
|
+
this.#flush();
|
|
3014
|
+
this.#setState("closed");
|
|
3015
|
+
}
|
|
3016
|
+
#kill() {
|
|
3017
|
+
const child = this.#child;
|
|
3018
|
+
this.#child = null;
|
|
3019
|
+
if (!child || child.exitCode !== null)
|
|
3020
|
+
return;
|
|
3021
|
+
try {
|
|
3022
|
+
child.stdin.end();
|
|
3023
|
+
} catch {
|
|
3024
|
+
}
|
|
3025
|
+
if (process.platform === "win32" && child.pid) {
|
|
3026
|
+
execFile3("taskkill", ["/pid", String(child.pid), "/T", "/F"], { windowsHide: true }, () => {
|
|
3027
|
+
if (child.exitCode === null)
|
|
3028
|
+
child.kill();
|
|
3029
|
+
});
|
|
3030
|
+
} else {
|
|
3031
|
+
child.kill("SIGTERM");
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
/** Новый процесс на том же разговоре — смена настроек, ручная починка. */
|
|
3035
|
+
#restart() {
|
|
3036
|
+
if (this.#closing)
|
|
3037
|
+
return;
|
|
3038
|
+
this.#restartPending = false;
|
|
3039
|
+
this.#kill();
|
|
3040
|
+
this.#pendingTurns = 0;
|
|
3041
|
+
this.#spawn();
|
|
3042
|
+
if (this.#state !== "error" && this.#state !== "closed")
|
|
3043
|
+
this.#setState("idle");
|
|
3044
|
+
}
|
|
3045
|
+
reconnect(_reason) {
|
|
3046
|
+
this.#restart();
|
|
3047
|
+
}
|
|
3048
|
+
// -------------------------------------------------------------------------
|
|
3049
|
+
// Ввод
|
|
3050
|
+
// -------------------------------------------------------------------------
|
|
3051
|
+
sendPrompt(text, images) {
|
|
3052
|
+
if (this.#closing)
|
|
3053
|
+
throw new Error("session is closed");
|
|
3054
|
+
const command = /^\/([\w-]+)(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
3055
|
+
if (command && AGY_CLI_COMMANDS.has(command[1]) && (!images || images.length === 0)) {
|
|
3056
|
+
void this.#runCliCommand(text, command[1], command[2]?.trim() ?? "");
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
this.#record_({ kind: "user.message", text, images });
|
|
3060
|
+
if (!this.#record.title || this.#titleIsDefault) {
|
|
3061
|
+
this.#record.title = text.slice(0, 80);
|
|
3062
|
+
this.#titleIsDefault = false;
|
|
3063
|
+
}
|
|
3064
|
+
this.#lastTurn = null;
|
|
3065
|
+
if (!this.#child)
|
|
3066
|
+
this.#spawn();
|
|
3067
|
+
if (!this.#child)
|
|
3068
|
+
return;
|
|
3069
|
+
this.#pendingTurns += 1;
|
|
3070
|
+
this.#setState("running");
|
|
3071
|
+
const child = this.#child;
|
|
3072
|
+
void this.#composePrompt(text, images).then((prompt) => {
|
|
3073
|
+
if (this.#child !== child)
|
|
3074
|
+
return;
|
|
3075
|
+
const line = JSON.stringify({
|
|
3076
|
+
event: "user",
|
|
3077
|
+
message: { role: "user", content: [{ type: "text", text: prompt }] }
|
|
3078
|
+
});
|
|
3079
|
+
child.stdin.write(line + "\n");
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Встроенная команда `agy` — отдельным вызовом, без хода модели.
|
|
3084
|
+
*
|
|
3085
|
+
* `/model` и `/effort` с аргументом меняют настройки сессии ровно как
|
|
3086
|
+
* дропдауны (своё значение у `agy -p` прожило бы один процесс), без
|
|
3087
|
+
* аргумента — называют текущее. `/usage` заодно обновляет шкалу в шапке.
|
|
3088
|
+
* Остальное — как ответил сам `agy`. Посреди хода команда отвечает в ленту,
|
|
3089
|
+
* но ни состояния, ни `result` не трогает: ход закроет его собственный.
|
|
3090
|
+
*/
|
|
3091
|
+
async #runCliCommand(text, name, arg) {
|
|
3092
|
+
this.#record_({ kind: "user.message", text });
|
|
3093
|
+
const turnRunning = this.#pendingTurns > 0;
|
|
3094
|
+
if (!turnRunning) {
|
|
3095
|
+
this.#lastTurn = null;
|
|
3096
|
+
this.#setState("running");
|
|
3097
|
+
}
|
|
3098
|
+
const started = Date.now();
|
|
3099
|
+
let reply = null;
|
|
3100
|
+
let error = null;
|
|
3101
|
+
try {
|
|
3102
|
+
if (!this.#executable) {
|
|
3103
|
+
error = AGY_MISSING_HINT;
|
|
3104
|
+
} else if (name === "model") {
|
|
3105
|
+
({ reply, error } = await this.#modelCommand(arg));
|
|
3106
|
+
} else if (name === "effort") {
|
|
3107
|
+
({ reply, error } = await this.#effortCommand(arg));
|
|
3108
|
+
} else if (name === "usage" || name === "quota") {
|
|
3109
|
+
const usage = await agyUsage(this.#executable, true);
|
|
3110
|
+
if (usage) {
|
|
3111
|
+
this.#usage = usage;
|
|
3112
|
+
this.emit("usage", usage);
|
|
3113
|
+
reply = formatUsage(usage);
|
|
3114
|
+
} else {
|
|
3115
|
+
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"}`;
|
|
3116
|
+
}
|
|
3117
|
+
} else {
|
|
3118
|
+
const result = await agyCommand(this.#executable, `/${name}${arg ? ` ${arg}` : ""}`, this.cwd);
|
|
3119
|
+
if (result.status === "SUCCESS")
|
|
3120
|
+
reply = formatCommandReply(name, result.response ?? "");
|
|
3121
|
+
else
|
|
3122
|
+
error = result.error || `/${name} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u043E\u0448\u0438\u0431\u043A\u043E\u0439`;
|
|
3123
|
+
}
|
|
3124
|
+
} catch (e) {
|
|
3125
|
+
error = describe2(e);
|
|
3126
|
+
}
|
|
3127
|
+
if (this.#closing)
|
|
3128
|
+
return;
|
|
3129
|
+
if (reply)
|
|
3130
|
+
this.#record_({ kind: "assistant.text", text: reply, blockId: `cmd:${randomUUID2()}` });
|
|
3131
|
+
if (error)
|
|
3132
|
+
this.#record_({ kind: "error", message: error });
|
|
3133
|
+
if (turnRunning || this.#pendingTurns > 0)
|
|
3134
|
+
return;
|
|
3135
|
+
this.#record_({
|
|
3136
|
+
kind: "result",
|
|
3137
|
+
subtype: error ? "error" : "success",
|
|
3138
|
+
isError: Boolean(error),
|
|
3139
|
+
durationMs: Date.now() - started,
|
|
3140
|
+
costUsd: null,
|
|
3141
|
+
usage: null,
|
|
3142
|
+
contextTokens: this.#contextTokens,
|
|
3143
|
+
text: reply
|
|
3144
|
+
});
|
|
3145
|
+
this.#lastTurn = error ? "failed" : "completed";
|
|
3146
|
+
if (this.#state === "running")
|
|
3147
|
+
this.#setState("idle");
|
|
3148
|
+
else
|
|
3149
|
+
this.emit("summary", this.summary());
|
|
3150
|
+
}
|
|
3151
|
+
async #modelCommand(arg) {
|
|
3152
|
+
const models = this.#capabilities?.models ?? [];
|
|
3153
|
+
const label = (id2) => id2 ? models.find((m) => m.value === id2)?.displayName ?? id2 : "\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E agy";
|
|
3154
|
+
if (!arg) {
|
|
3155
|
+
const current = this.#record.model ?? this.#activeModel;
|
|
3156
|
+
return { reply: `\u041C\u043E\u0434\u0435\u043B\u044C: **${label(current)}**${current ? ` (\`${current}\`)` : ""}`, error: null };
|
|
3157
|
+
}
|
|
3158
|
+
const wanted = arg.toLowerCase();
|
|
3159
|
+
const match = models.find((m) => m.value.toLowerCase() === wanted) ?? models.find((m) => m.displayName.toLowerCase() === wanted);
|
|
3160
|
+
if (models.length > 0 && !match) {
|
|
3161
|
+
return {
|
|
3162
|
+
reply: null,
|
|
3163
|
+
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(", ")}`
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
const id = match?.value ?? arg;
|
|
3167
|
+
await this.setModel(id);
|
|
3168
|
+
return { reply: `\u041C\u043E\u0434\u0435\u043B\u044C \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0430: **${label(id)}** (\`${id}\`)`, error: null };
|
|
3169
|
+
}
|
|
3170
|
+
async #effortCommand(arg) {
|
|
3171
|
+
const levels = ["low", "medium", "high", "max"];
|
|
3172
|
+
if (!arg) {
|
|
3173
|
+
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 };
|
|
3174
|
+
}
|
|
3175
|
+
const level = arg.toLowerCase();
|
|
3176
|
+
if (!levels.includes(level)) {
|
|
3177
|
+
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(", ")}` };
|
|
3178
|
+
}
|
|
3179
|
+
await this.setEffort(level);
|
|
3180
|
+
return { reply: `\u0423\u0441\u0438\u043B\u0438\u0435: **${level}**`, error: null };
|
|
3181
|
+
}
|
|
3182
|
+
/** Картинки — файлами во временном каталоге, а в текст — их пути. */
|
|
3183
|
+
async #composePrompt(text, images) {
|
|
3184
|
+
if (!images || images.length === 0)
|
|
3185
|
+
return text;
|
|
3186
|
+
const dir = path7.join(tmpdir(), "ccv-antigravity", this.id);
|
|
3187
|
+
await mkdir2(dir, { recursive: true });
|
|
3188
|
+
const paths = [];
|
|
3189
|
+
for (const image of images) {
|
|
3190
|
+
const ext = image.mediaType.split("/")[1] === "jpeg" ? "jpg" : image.mediaType.split("/")[1];
|
|
3191
|
+
const file = path7.join(dir, `${randomUUID2()}.${ext}`);
|
|
3192
|
+
await writeFile2(file, Buffer.from(image.data, "base64"));
|
|
3193
|
+
paths.push(file);
|
|
3194
|
+
}
|
|
3195
|
+
return `${text}
|
|
3196
|
+
|
|
3197
|
+
Attached image${paths.length > 1 ? "s" : ""} (view them with your file viewing tool):
|
|
3198
|
+
` + paths.map((p) => `- ${p}`).join("\n");
|
|
3199
|
+
}
|
|
3200
|
+
async editPrompt(_userIndex, _text, _images) {
|
|
3201
|
+
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");
|
|
3202
|
+
}
|
|
3203
|
+
respondPermission(_requestId, _decision, _reason, _answers, _annotations, _plan) {
|
|
3204
|
+
}
|
|
3205
|
+
async interrupt() {
|
|
3206
|
+
if (this.#pendingTurns === 0)
|
|
3207
|
+
return;
|
|
3208
|
+
this.#interrupting = true;
|
|
3209
|
+
this.#kill();
|
|
3210
|
+
this.#closeOpenSteps();
|
|
3211
|
+
this.#pendingTurns = 0;
|
|
3212
|
+
this.#lastTurn = "interrupted";
|
|
3213
|
+
this.#record_({
|
|
3214
|
+
kind: "result",
|
|
3215
|
+
subtype: "interrupted",
|
|
3216
|
+
isError: false,
|
|
3217
|
+
durationMs: 0,
|
|
3218
|
+
costUsd: null,
|
|
3219
|
+
usage: null,
|
|
3220
|
+
contextTokens: this.#contextTokens,
|
|
3221
|
+
text: null
|
|
3222
|
+
});
|
|
3223
|
+
this.#interrupting = false;
|
|
3224
|
+
this.#spawn();
|
|
3225
|
+
if (this.#state !== "error")
|
|
3226
|
+
this.#setState("idle");
|
|
3227
|
+
}
|
|
3228
|
+
async setPermissionMode(mode) {
|
|
3229
|
+
if (this.#record.permissionMode === mode)
|
|
3230
|
+
return;
|
|
3231
|
+
this.#record.permissionMode = mode;
|
|
3232
|
+
this.#settingsChanged();
|
|
3233
|
+
}
|
|
3234
|
+
async setModel(model) {
|
|
3235
|
+
if (this.#record.model === model)
|
|
3236
|
+
return;
|
|
3237
|
+
this.#record.model = model;
|
|
3238
|
+
this.#settingsChanged();
|
|
3239
|
+
}
|
|
3240
|
+
async setEffort(effort) {
|
|
3241
|
+
if (this.#record.effort === effort)
|
|
3242
|
+
return;
|
|
3243
|
+
this.#record.effort = effort;
|
|
3244
|
+
this.#settingsChanged();
|
|
3245
|
+
}
|
|
3246
|
+
/** Флаги запуска поменялись: новый процесс — сейчас или после хода. */
|
|
3247
|
+
#settingsChanged() {
|
|
3248
|
+
if (this.#capabilities) {
|
|
3249
|
+
this.#capabilities = this.#baseCapabilities();
|
|
3250
|
+
this.emit("capabilities");
|
|
3251
|
+
}
|
|
3252
|
+
this.emit("summary", this.summary());
|
|
3253
|
+
void this.#store.save(this.#record);
|
|
3254
|
+
if (!this.#executable || this.#state === "closed")
|
|
3255
|
+
return;
|
|
3256
|
+
if (this.#pendingTurns > 0)
|
|
3257
|
+
this.#restartPending = true;
|
|
3258
|
+
else
|
|
3259
|
+
this.#restart();
|
|
3260
|
+
}
|
|
3261
|
+
// Claude-only управление: у agy этого нет — молча ничего не делаем.
|
|
3262
|
+
async setUltracode(_enabled) {
|
|
3263
|
+
}
|
|
3264
|
+
async setThinking(_enabled) {
|
|
3265
|
+
}
|
|
3266
|
+
async setSwitchModelsOnFlag(_enabled) {
|
|
3267
|
+
}
|
|
3268
|
+
/** Квота `agy` (`/usage`) — общая на аккаунт, кэш один на хост. */
|
|
3269
|
+
async refreshUsage(force = false) {
|
|
3270
|
+
if (!this.#executable)
|
|
3271
|
+
return;
|
|
3272
|
+
const usage = await agyUsage(this.#executable, force);
|
|
3273
|
+
if (!usage)
|
|
3274
|
+
return;
|
|
3275
|
+
this.#usage = usage;
|
|
3276
|
+
this.emit("usage", usage);
|
|
3277
|
+
}
|
|
3278
|
+
async refreshContext(_force = false) {
|
|
3279
|
+
this.#emitContext();
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Снимок окна контекста: занятое — из `usage` последнего шага модели,
|
|
3283
|
+
* всего — по модели из таблицы. Чего-то из двух нет — молчим: полоса
|
|
3284
|
+
* останется со своим умолчанием и числом из `result`.
|
|
3285
|
+
*/
|
|
3286
|
+
#emitContext() {
|
|
3287
|
+
const maxTokens = agyContextWindow(this.#record.model ?? this.#activeModel);
|
|
3288
|
+
if (this.#contextTokens === null || maxTokens === null)
|
|
3289
|
+
return;
|
|
3290
|
+
this.#context = { tokens: this.#contextTokens, maxTokens, fetchedAt: Date.now() };
|
|
3291
|
+
this.emit("context", this.#context);
|
|
3292
|
+
}
|
|
3293
|
+
async reloadPlugins() {
|
|
3294
|
+
}
|
|
3295
|
+
async mcpServerStatus() {
|
|
3296
|
+
return [];
|
|
3297
|
+
}
|
|
3298
|
+
async addMcpServer(_name, _config) {
|
|
3299
|
+
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`)");
|
|
3300
|
+
}
|
|
3301
|
+
async removeMcpServer(_name) {
|
|
3302
|
+
return false;
|
|
3303
|
+
}
|
|
3304
|
+
setTitle(title) {
|
|
3305
|
+
this.#record.title = title;
|
|
3306
|
+
this.#titleIsDefault = false;
|
|
3307
|
+
void this.#store.save(this.#record);
|
|
3308
|
+
this.emit("summary", this.summary());
|
|
3309
|
+
}
|
|
3310
|
+
// -------------------------------------------------------------------------
|
|
3311
|
+
// Поток agy
|
|
3312
|
+
// -------------------------------------------------------------------------
|
|
3313
|
+
#handleLine(line) {
|
|
3314
|
+
const trimmed = line.trim();
|
|
3315
|
+
if (!trimmed.startsWith("{"))
|
|
3316
|
+
return;
|
|
3317
|
+
let message;
|
|
3318
|
+
try {
|
|
3319
|
+
message = JSON.parse(trimmed);
|
|
3320
|
+
} catch {
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
switch (message.event) {
|
|
3324
|
+
case "init": {
|
|
3325
|
+
const conversationId = typeof message.conversation_id === "string" ? message.conversation_id : null;
|
|
3326
|
+
if (conversationId && conversationId !== this.#record.conversationId) {
|
|
3327
|
+
this.#record.conversationId = conversationId;
|
|
3328
|
+
void this.#store.save(this.#record);
|
|
3329
|
+
}
|
|
3330
|
+
const init = message.init;
|
|
3331
|
+
if (typeof init?.model === "string" && init.model && init.model !== this.#activeModel) {
|
|
3332
|
+
this.#activeModel = init.model;
|
|
3333
|
+
if (this.#capabilities) {
|
|
3334
|
+
this.#capabilities = this.#baseCapabilities();
|
|
3335
|
+
this.emit("capabilities");
|
|
3336
|
+
}
|
|
3337
|
+
this.#emitContext();
|
|
3338
|
+
}
|
|
3339
|
+
if (this.#capabilities && Array.isArray(init?.tools)) {
|
|
3340
|
+
this.#capabilities = {
|
|
3341
|
+
...this.#capabilities,
|
|
3342
|
+
tools: init.tools.filter((t) => typeof t === "string")
|
|
3343
|
+
};
|
|
3344
|
+
this.emit("capabilities");
|
|
3345
|
+
}
|
|
3346
|
+
if (this.#state === "starting")
|
|
3347
|
+
this.#setState("idle");
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
case "step_update":
|
|
3351
|
+
this.#handleStep(message.step_update);
|
|
3352
|
+
return;
|
|
3353
|
+
case "result":
|
|
3354
|
+
this.#handleResult(message.result);
|
|
3355
|
+
return;
|
|
3356
|
+
default:
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
#handleStep(step2) {
|
|
3361
|
+
if (!step2 || typeof step2.step_index !== "number")
|
|
3362
|
+
return;
|
|
3363
|
+
const index = step2.step_index;
|
|
3364
|
+
const blockId = `${this.#record.conversationId ?? this.id}:${index}`;
|
|
3365
|
+
if (step2.step_type === "user_input")
|
|
3366
|
+
return;
|
|
3367
|
+
if (step2.step_type === "tool" || step2.tool_info || step2.tool_name) {
|
|
3368
|
+
const name = step2.tool_name ?? step2.tool_info?.name ?? "tool";
|
|
3369
|
+
if (!this.#tools.has(index)) {
|
|
3370
|
+
this.#tools.add(index);
|
|
3371
|
+
this.#record_({
|
|
3372
|
+
kind: "tool.use",
|
|
3373
|
+
toolUseId: blockId,
|
|
3374
|
+
name,
|
|
3375
|
+
input: toolInput(step2.tool_info?.parameters)
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
if (step2.state === "DONE" || step2.state === "ERROR") {
|
|
3379
|
+
const error = step2.tool_info?.error?.message;
|
|
3380
|
+
this.#record_({
|
|
3381
|
+
kind: "tool.result",
|
|
3382
|
+
toolUseId: blockId,
|
|
3383
|
+
content: error ?? step2.tool_info?.output ?? "",
|
|
3384
|
+
isError: step2.state === "ERROR" || Boolean(error)
|
|
3385
|
+
});
|
|
3386
|
+
this.#tools.delete(index);
|
|
3387
|
+
}
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
if (step2.thinking_delta) {
|
|
3391
|
+
this.#thinking.set(index, (this.#thinking.get(index) ?? "") + step2.thinking_delta);
|
|
3392
|
+
this.#record_({ kind: "thinking.delta", text: step2.thinking_delta, blockId: `${blockId}:t` });
|
|
3393
|
+
}
|
|
3394
|
+
if (step2.text_delta) {
|
|
3395
|
+
this.#texts.set(index, (this.#texts.get(index) ?? "") + step2.text_delta);
|
|
3396
|
+
this.#record_({ kind: "assistant.delta", text: step2.text_delta, blockId });
|
|
3397
|
+
}
|
|
3398
|
+
if (step2.usage && typeof step2.usage.input_tokens === "number") {
|
|
3399
|
+
this.#contextTokens = step2.usage.input_tokens + (step2.usage.output_tokens ?? 0);
|
|
3400
|
+
}
|
|
3401
|
+
if (step2.state === "DONE" || step2.state === "ERROR") {
|
|
3402
|
+
const thinking = this.#thinking.get(index);
|
|
3403
|
+
if (thinking) {
|
|
3404
|
+
this.#record_({ kind: "assistant.thinking", text: thinking, blockId: `${blockId}:t` });
|
|
3405
|
+
this.#thinking.delete(index);
|
|
3406
|
+
}
|
|
3407
|
+
const text = this.#texts.get(index);
|
|
3408
|
+
if (text) {
|
|
3409
|
+
this.#record_({ kind: "assistant.text", text: text.replace(/\n+$/, ""), blockId });
|
|
3410
|
+
this.#texts.delete(index);
|
|
3411
|
+
}
|
|
3412
|
+
if (step2.state === "ERROR") {
|
|
3413
|
+
const message = typeof step2.error === "string" ? step2.error : step2.error?.message;
|
|
3414
|
+
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` });
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
#handleResult(result) {
|
|
3419
|
+
if (!result || this.#interrupting)
|
|
3420
|
+
return;
|
|
3421
|
+
this.#closeOpenSteps();
|
|
3422
|
+
const ok = result.status === "SUCCESS";
|
|
3423
|
+
const usage = result.usage ? {
|
|
3424
|
+
inputTokens: result.usage.input_tokens ?? 0,
|
|
3425
|
+
outputTokens: (result.usage.output_tokens ?? 0) + (result.usage.thinking_tokens ?? 0),
|
|
3426
|
+
cacheReadTokens: result.usage.cache_read_tokens ?? 0,
|
|
3427
|
+
cacheCreationTokens: 0
|
|
3428
|
+
} : null;
|
|
3429
|
+
const denied = (result.denied_actions ?? []).map((a) => a.display_name ?? a.action).filter((a) => Boolean(a));
|
|
3430
|
+
if (denied.length > 0) {
|
|
3431
|
+
this.#record_({
|
|
3432
|
+
kind: "error",
|
|
3433
|
+
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).`
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3436
|
+
if (!ok && result.error)
|
|
3437
|
+
this.#record_({ kind: "error", message: result.error });
|
|
3438
|
+
this.#record_({
|
|
3439
|
+
kind: "result",
|
|
3440
|
+
subtype: ok ? "success" : "error",
|
|
3441
|
+
isError: !ok,
|
|
3442
|
+
durationMs: Math.round((result.duration_seconds ?? 0) * 1e3),
|
|
3443
|
+
costUsd: null,
|
|
3444
|
+
usage,
|
|
3445
|
+
contextTokens: this.#contextTokens,
|
|
3446
|
+
text: result.response ?? null
|
|
3447
|
+
});
|
|
3448
|
+
this.#emitContext();
|
|
3449
|
+
void this.refreshUsage(true);
|
|
3450
|
+
this.#lastTurn = ok ? "completed" : "failed";
|
|
3451
|
+
this.#pendingTurns = Math.max(0, this.#pendingTurns - 1);
|
|
3452
|
+
if (this.#pendingTurns > 0)
|
|
3453
|
+
return;
|
|
3454
|
+
if (this.#restartPending)
|
|
3455
|
+
this.#restart();
|
|
3456
|
+
this.#setState("idle");
|
|
3457
|
+
}
|
|
3458
|
+
/** Незакрытые шаги оборванного хода: текст — в ленту, инструменты — с ошибкой. */
|
|
3459
|
+
#closeOpenSteps() {
|
|
3460
|
+
const prefix = this.#record.conversationId ?? this.id;
|
|
3461
|
+
for (const [index, text] of this.#texts) {
|
|
3462
|
+
if (text)
|
|
3463
|
+
this.#record_({ kind: "assistant.text", text, blockId: `${prefix}:${index}` });
|
|
3464
|
+
}
|
|
3465
|
+
for (const index of this.#tools) {
|
|
3466
|
+
this.#record_({
|
|
3467
|
+
kind: "tool.result",
|
|
3468
|
+
toolUseId: `${prefix}:${index}`,
|
|
3469
|
+
content: "\u041F\u0440\u0435\u0440\u0432\u0430\u043D\u043E",
|
|
3470
|
+
isError: true
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
this.#texts.clear();
|
|
3474
|
+
this.#thinking.clear();
|
|
3475
|
+
this.#tools.clear();
|
|
3476
|
+
}
|
|
3477
|
+
// -------------------------------------------------------------------------
|
|
3478
|
+
#record_(event) {
|
|
3479
|
+
this.#updatedAt = Date.now();
|
|
3480
|
+
const entry = this.journal.append(event);
|
|
3481
|
+
this.emit("event", entry);
|
|
3482
|
+
if (TRANSIENT.has(event.kind))
|
|
3483
|
+
return;
|
|
3484
|
+
this.#unsaved.push({ ts: entry.ts, event });
|
|
3485
|
+
this.#scheduleFlush();
|
|
3486
|
+
}
|
|
3487
|
+
#scheduleFlush() {
|
|
3488
|
+
if (this.#flushTimer)
|
|
3489
|
+
return;
|
|
3490
|
+
this.#flushTimer = setTimeout(() => this.#flush(), 500);
|
|
3491
|
+
this.#flushTimer.unref?.();
|
|
3492
|
+
}
|
|
3493
|
+
#flush() {
|
|
3494
|
+
if (this.#flushTimer) {
|
|
3495
|
+
clearTimeout(this.#flushTimer);
|
|
3496
|
+
this.#flushTimer = null;
|
|
3497
|
+
}
|
|
3498
|
+
const batch = this.#unsaved;
|
|
3499
|
+
this.#unsaved = [];
|
|
3500
|
+
this.#record.updatedAt = this.#updatedAt;
|
|
3501
|
+
void this.#store.appendEvents(this.id, batch);
|
|
3502
|
+
void this.#store.save(this.#record);
|
|
3503
|
+
}
|
|
3504
|
+
#setState(state) {
|
|
3505
|
+
if (this.#state === state)
|
|
3506
|
+
return;
|
|
3507
|
+
this.#state = state;
|
|
3508
|
+
this.#record_({ kind: "state", state });
|
|
3509
|
+
if (state === "idle" || state === "error" || state === "closed")
|
|
3510
|
+
this.#flush();
|
|
3511
|
+
this.emit("summary", this.summary());
|
|
3512
|
+
}
|
|
3513
|
+
};
|
|
3514
|
+
function toolInput(parameters) {
|
|
3515
|
+
const input = { ...parameters ?? {} };
|
|
3516
|
+
const pick = (...keys) => keys.map((k) => input[k]).find((v) => typeof v === "string");
|
|
3517
|
+
const file = pick("TargetFile", "AbsolutePath", "File", "FilePath", "Path");
|
|
3518
|
+
if (file && input.file_path === void 0)
|
|
3519
|
+
input.file_path = file;
|
|
3520
|
+
const command = pick("CommandLine", "Command");
|
|
3521
|
+
if (command && input.command === void 0)
|
|
3522
|
+
input.command = command;
|
|
3523
|
+
const pattern = pick("Query", "Pattern", "SearchPattern");
|
|
3524
|
+
if (pattern && input.pattern === void 0)
|
|
3525
|
+
input.pattern = pattern;
|
|
3526
|
+
const dir = pick("DirectoryPath", "SearchPath", "SearchDirectory");
|
|
3527
|
+
if (dir && input.path === void 0)
|
|
3528
|
+
input.path = dir;
|
|
3529
|
+
const url2 = pick("Url", "URL");
|
|
3530
|
+
if (url2 && input.url === void 0)
|
|
3531
|
+
input.url = url2;
|
|
3532
|
+
return input;
|
|
3533
|
+
}
|
|
3534
|
+
function formatUsage(usage) {
|
|
3535
|
+
const rows = (usage.buckets ?? []).map((b) => {
|
|
3536
|
+
const left = Math.round(100 - b.window.utilization);
|
|
3537
|
+
const reset = b.window.resetsAt ? new Date(b.window.resetsAt).toLocaleString("ru-RU") : "\u2014";
|
|
3538
|
+
return `| ${b.title || b.label} | ${left}% | ${reset} |`;
|
|
3539
|
+
});
|
|
3540
|
+
if (rows.length === 0)
|
|
3541
|
+
return "\u041A\u0432\u043E\u0442\u0430 Antigravity: \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435\u0442.";
|
|
3542
|
+
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");
|
|
3543
|
+
}
|
|
3544
|
+
function formatCommandReply(name, response) {
|
|
3545
|
+
const text = response.replace(/\s+$/, "");
|
|
3546
|
+
if (!text)
|
|
3547
|
+
return `/${name}: \u043F\u0443\u0441\u0442\u043E`;
|
|
3548
|
+
const lines = text.split(/\r?\n/);
|
|
3549
|
+
if (lines.every((l) => l.includes(" "))) {
|
|
3550
|
+
return lines.map((l) => {
|
|
3551
|
+
const [head, ...rest] = l.split(" ");
|
|
3552
|
+
return `- **${head}** \u2014 ${rest.join(" ")}`;
|
|
3553
|
+
}).join("\n");
|
|
3554
|
+
}
|
|
3555
|
+
return text;
|
|
3556
|
+
}
|
|
3557
|
+
function agyEffort(effort) {
|
|
3558
|
+
if (!effort)
|
|
3559
|
+
return null;
|
|
3560
|
+
return effort === "xhigh" ? "high" : effort;
|
|
3561
|
+
}
|
|
3562
|
+
function describe2(error) {
|
|
3563
|
+
return error instanceof Error ? error.message : String(error);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// ../host/dist/antigravity-store.js
|
|
3567
|
+
import { appendFile, mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
3568
|
+
import path8 from "node:path";
|
|
3569
|
+
var AntigravityStore = class {
|
|
3570
|
+
#dir;
|
|
3571
|
+
#file;
|
|
3572
|
+
#records = /* @__PURE__ */ new Map();
|
|
3573
|
+
#loaded = null;
|
|
3574
|
+
#writing = Promise.resolve();
|
|
3575
|
+
constructor(dataDir) {
|
|
3576
|
+
this.#dir = path8.join(dataDir, "antigravity");
|
|
3577
|
+
this.#file = path8.join(this.#dir, "sessions.json");
|
|
3578
|
+
}
|
|
3579
|
+
#load() {
|
|
3580
|
+
this.#loaded ??= (async () => {
|
|
3581
|
+
try {
|
|
3582
|
+
const raw = JSON.parse(await readFile4(this.#file, "utf8"));
|
|
3583
|
+
for (const record of Array.isArray(raw) ? raw : []) {
|
|
3584
|
+
if (record && typeof record.id === "string")
|
|
3585
|
+
this.#records.set(record.id, record);
|
|
3586
|
+
}
|
|
3587
|
+
} catch {
|
|
3588
|
+
}
|
|
3589
|
+
})();
|
|
3590
|
+
return this.#loaded;
|
|
3591
|
+
}
|
|
3592
|
+
async find(id) {
|
|
3593
|
+
await this.#load();
|
|
3594
|
+
return this.#records.get(id) ?? null;
|
|
3595
|
+
}
|
|
3596
|
+
async list(workspaceId) {
|
|
3597
|
+
await this.#load();
|
|
3598
|
+
return [...this.#records.values()].filter((r) => r.workspaceId === workspaceId);
|
|
3599
|
+
}
|
|
3600
|
+
async save(record) {
|
|
3601
|
+
await this.#load();
|
|
3602
|
+
this.#records.set(record.id, { ...record });
|
|
3603
|
+
await this.#persist();
|
|
3604
|
+
}
|
|
3605
|
+
async rename(id, title) {
|
|
3606
|
+
await this.#load();
|
|
3607
|
+
const record = this.#records.get(id);
|
|
3608
|
+
if (!record)
|
|
3609
|
+
return false;
|
|
3610
|
+
record.title = title;
|
|
3611
|
+
await this.#persist();
|
|
3612
|
+
return true;
|
|
3613
|
+
}
|
|
3614
|
+
async remove(id) {
|
|
3615
|
+
await this.#load();
|
|
3616
|
+
if (!this.#records.delete(id))
|
|
3617
|
+
return;
|
|
3618
|
+
await this.#persist();
|
|
3619
|
+
await rm2(this.#eventsFile(id), { force: true });
|
|
3620
|
+
}
|
|
3621
|
+
/** Дописать события в ленту сессии. Порядок записей сохраняется. */
|
|
3622
|
+
appendEvents(id, events) {
|
|
3623
|
+
if (events.length === 0)
|
|
3624
|
+
return this.#writing;
|
|
3625
|
+
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
3626
|
+
return this.#enqueue(async () => {
|
|
3627
|
+
await mkdir3(this.#dir, { recursive: true });
|
|
3628
|
+
await appendFile(this.#eventsFile(id), lines, "utf8");
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
/** Лента сессии как история: `seq` — позиция, от 1 по порядку. */
|
|
3632
|
+
async readEvents(id) {
|
|
3633
|
+
await this.#writing;
|
|
3634
|
+
let raw;
|
|
3635
|
+
try {
|
|
3636
|
+
raw = await readFile4(this.#eventsFile(id), "utf8");
|
|
3637
|
+
} catch {
|
|
3638
|
+
return [];
|
|
2471
3639
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
3640
|
+
const entries = [];
|
|
3641
|
+
for (const line of raw.split("\n")) {
|
|
3642
|
+
if (!line.trim())
|
|
3643
|
+
continue;
|
|
3644
|
+
try {
|
|
3645
|
+
const parsed = JSON.parse(line);
|
|
3646
|
+
entries.push({ seq: entries.length + 1, ts: parsed.ts, event: parsed.event });
|
|
3647
|
+
} catch {
|
|
3648
|
+
}
|
|
2474
3649
|
}
|
|
2475
|
-
return
|
|
2476
|
-
}
|
|
2477
|
-
|
|
3650
|
+
return entries;
|
|
3651
|
+
}
|
|
3652
|
+
#eventsFile(id) {
|
|
3653
|
+
return path8.join(this.#dir, `${id.replace(/[^\w-]/g, "_")}.jsonl`);
|
|
3654
|
+
}
|
|
3655
|
+
#persist() {
|
|
3656
|
+
const snapshot = JSON.stringify([...this.#records.values()], null, 2);
|
|
3657
|
+
return this.#enqueue(async () => {
|
|
3658
|
+
await mkdir3(this.#dir, { recursive: true });
|
|
3659
|
+
await writeFile3(this.#file, snapshot, "utf8");
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
/** Записи на диск — строго по очереди: две параллельные испортили бы файл. */
|
|
3663
|
+
#enqueue(task) {
|
|
3664
|
+
const next = this.#writing.then(task, task);
|
|
3665
|
+
this.#writing = next.catch((error) => {
|
|
3666
|
+
console.error("[antigravity-store]", error instanceof Error ? error.message : error);
|
|
3667
|
+
});
|
|
3668
|
+
return this.#writing;
|
|
3669
|
+
}
|
|
3670
|
+
};
|
|
2478
3671
|
|
|
2479
3672
|
// ../host/dist/git.js
|
|
2480
|
-
import { execFile as
|
|
2481
|
-
import { mkdtemp, readFile as
|
|
2482
|
-
import { tmpdir } from "node:os";
|
|
2483
|
-
import
|
|
3673
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3674
|
+
import { mkdtemp, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
3675
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
3676
|
+
import path9 from "node:path";
|
|
2484
3677
|
var TIMEOUT_MS = 2e4;
|
|
2485
3678
|
var NET_TIMEOUT_MS = 5 * 6e4;
|
|
2486
3679
|
var MAX_BUFFER = 32 * 1024 * 1024;
|
|
@@ -2526,7 +3719,7 @@ var GitRepo = class {
|
|
|
2526
3719
|
await this.#run(["rev-parse", "--is-inside-work-tree"]);
|
|
2527
3720
|
} catch (error) {
|
|
2528
3721
|
const runnable2 = await this.#run(["--version"]).then(() => true, () => false);
|
|
2529
|
-
return { ...blank, message:
|
|
3722
|
+
return { ...blank, message: describe3(error), noRepo: runnable2 };
|
|
2530
3723
|
}
|
|
2531
3724
|
const branchRaw = await this.#soft(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2532
3725
|
const detached = branchRaw === "HEAD" || branchRaw === "";
|
|
@@ -2796,12 +3989,12 @@ var GitRepo = class {
|
|
|
2796
3989
|
* важнее всего (см. `checkIgnored`).
|
|
2797
3990
|
*/
|
|
2798
3991
|
async #withoutRepo(args) {
|
|
2799
|
-
const dir = await mkdtemp(
|
|
3992
|
+
const dir = await mkdtemp(path9.join(tmpdir2(), "ccv-git-"));
|
|
2800
3993
|
try {
|
|
2801
3994
|
await this.#run(["init", "--bare", "--quiet", dir]);
|
|
2802
3995
|
return await this.#soft([`--git-dir=${dir}`, `--work-tree=${this.cwd}`, ...args]);
|
|
2803
3996
|
} finally {
|
|
2804
|
-
await
|
|
3997
|
+
await rm3(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2805
3998
|
}
|
|
2806
3999
|
}
|
|
2807
4000
|
/**
|
|
@@ -2842,14 +4035,14 @@ var GitRepo = class {
|
|
|
2842
4035
|
* исключил. Повторы отсеиваются, чтобы файл не рос от каждой публикации.
|
|
2843
4036
|
*/
|
|
2844
4037
|
async #appendGitignore(paths) {
|
|
2845
|
-
const file =
|
|
2846
|
-
const existing = await
|
|
4038
|
+
const file = path9.join(this.cwd, ".gitignore");
|
|
4039
|
+
const existing = await readFile5(file, "utf8").catch(() => "");
|
|
2847
4040
|
const known = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
|
|
2848
4041
|
const lines = paths.map(ignorePattern).filter((line) => !known.has(line));
|
|
2849
4042
|
if (lines.length === 0)
|
|
2850
4043
|
return;
|
|
2851
4044
|
const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
2852
|
-
await
|
|
4045
|
+
await writeFile4(file, `${existing}${prefix}${lines.join("\n")}
|
|
2853
4046
|
`, "utf8");
|
|
2854
4047
|
}
|
|
2855
4048
|
/** Команда, чей провал — обычное дело (нет upstream, нет коммитов, detached). */
|
|
@@ -2867,7 +4060,7 @@ var GitRepo = class {
|
|
|
2867
4060
|
#run(args, options2 = {}) {
|
|
2868
4061
|
const token = options2.token ?? "";
|
|
2869
4062
|
return new Promise((resolve, reject) => {
|
|
2870
|
-
const child =
|
|
4063
|
+
const child = execFile4(
|
|
2871
4064
|
"git",
|
|
2872
4065
|
// core.quotepath=false — иначе кириллические пути приезжают в виде
|
|
2873
4066
|
// «\320\277\321\200…» и в панели читаются как мусор.
|
|
@@ -2938,7 +4131,7 @@ function requirePath(value) {
|
|
|
2938
4131
|
function ignorePattern(relPath) {
|
|
2939
4132
|
return `/${relPath.replace(/[\\*?[\]]/g, (char) => `\\${char}`)}`;
|
|
2940
4133
|
}
|
|
2941
|
-
function
|
|
4134
|
+
function describe3(error) {
|
|
2942
4135
|
return error instanceof Error ? error.message : String(error);
|
|
2943
4136
|
}
|
|
2944
4137
|
function parseStatus(raw) {
|
|
@@ -3048,9 +4241,9 @@ function parsePatch(raw) {
|
|
|
3048
4241
|
body = "";
|
|
3049
4242
|
}
|
|
3050
4243
|
total += body.length;
|
|
3051
|
-
const
|
|
4244
|
+
const path25 = status === "deleted" ? oldPath : newPath || oldPath;
|
|
3052
4245
|
files.push({
|
|
3053
|
-
path:
|
|
4246
|
+
path: path25,
|
|
3054
4247
|
oldPath: status === "renamed" ? oldPath : "",
|
|
3055
4248
|
status,
|
|
3056
4249
|
added: count(body, "+"),
|
|
@@ -3062,10 +4255,10 @@ function parsePatch(raw) {
|
|
|
3062
4255
|
return { files, truncated };
|
|
3063
4256
|
}
|
|
3064
4257
|
function stripPrefix(value) {
|
|
3065
|
-
const
|
|
3066
|
-
if (
|
|
4258
|
+
const path25 = unquote(value.trim());
|
|
4259
|
+
if (path25 === "/dev/null")
|
|
3067
4260
|
return "";
|
|
3068
|
-
return
|
|
4261
|
+
return path25.replace(/^[ab]\//, "");
|
|
3069
4262
|
}
|
|
3070
4263
|
function count(body, mark) {
|
|
3071
4264
|
let n = 0;
|
|
@@ -3077,9 +4270,9 @@ function count(body, mark) {
|
|
|
3077
4270
|
}
|
|
3078
4271
|
|
|
3079
4272
|
// ../host/dist/github.js
|
|
3080
|
-
import { execFile as
|
|
3081
|
-
import { mkdir as
|
|
3082
|
-
import
|
|
4273
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
4274
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
|
|
4275
|
+
import path10 from "node:path";
|
|
3083
4276
|
var API = "https://api.github.com";
|
|
3084
4277
|
var API_VERSION = "2022-11-28";
|
|
3085
4278
|
var API_TIMEOUT_MS = 2e4;
|
|
@@ -3100,11 +4293,11 @@ var GitHubAccount = class {
|
|
|
3100
4293
|
*/
|
|
3101
4294
|
#ghInstalled = null;
|
|
3102
4295
|
constructor(dataDir) {
|
|
3103
|
-
this.#storePath =
|
|
4296
|
+
this.#storePath = path10.join(dataDir, "github.json");
|
|
3104
4297
|
}
|
|
3105
4298
|
async load() {
|
|
3106
4299
|
try {
|
|
3107
|
-
const raw = JSON.parse(await
|
|
4300
|
+
const raw = JSON.parse(await readFile6(this.#storePath, "utf8"));
|
|
3108
4301
|
const token = raw.token;
|
|
3109
4302
|
this.#token = typeof token === "string" ? token.trim() : "";
|
|
3110
4303
|
} catch {
|
|
@@ -3114,8 +4307,8 @@ var GitHubAccount = class {
|
|
|
3114
4307
|
/** Сохранить токен или стереть его пустой строкой. */
|
|
3115
4308
|
async setToken(token) {
|
|
3116
4309
|
this.#token = token.trim();
|
|
3117
|
-
await
|
|
3118
|
-
await
|
|
4310
|
+
await mkdir4(path10.dirname(this.#storePath), { recursive: true });
|
|
4311
|
+
await writeFile5(this.#storePath, JSON.stringify({ token: this.#token }, null, 2), {
|
|
3119
4312
|
encoding: "utf8",
|
|
3120
4313
|
mode: 384
|
|
3121
4314
|
});
|
|
@@ -3142,7 +4335,7 @@ var GitHubAccount = class {
|
|
|
3142
4335
|
const credential = await this.credential();
|
|
3143
4336
|
return { ...base, connected: true, source: credential.source, login: credential.login };
|
|
3144
4337
|
} catch (error) {
|
|
3145
|
-
return { ...base, message:
|
|
4338
|
+
return { ...base, message: describe4(error) };
|
|
3146
4339
|
}
|
|
3147
4340
|
}
|
|
3148
4341
|
/**
|
|
@@ -3240,7 +4433,7 @@ var GitHubAccount = class {
|
|
|
3240
4433
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
3241
4434
|
});
|
|
3242
4435
|
} catch (error) {
|
|
3243
|
-
throw new Error(`GitHub \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${
|
|
4436
|
+
throw new Error(`GitHub \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${describe4(error)}`);
|
|
3244
4437
|
}
|
|
3245
4438
|
const text = await response.text();
|
|
3246
4439
|
if (!response.ok)
|
|
@@ -3266,7 +4459,7 @@ var GitHubAccount = class {
|
|
|
3266
4459
|
};
|
|
3267
4460
|
function gh(args) {
|
|
3268
4461
|
return new Promise((resolve, reject) => {
|
|
3269
|
-
|
|
4462
|
+
execFile5("gh", args, { timeout: GH_TIMEOUT_MS, windowsHide: true, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
3270
4463
|
if (error)
|
|
3271
4464
|
return reject(new Error(stderr.trim() || error.message));
|
|
3272
4465
|
resolve(stdout);
|
|
@@ -3302,7 +4495,7 @@ function apiError(status, body) {
|
|
|
3302
4495
|
function str(value) {
|
|
3303
4496
|
return typeof value === "string" ? value : "";
|
|
3304
4497
|
}
|
|
3305
|
-
function
|
|
4498
|
+
function describe4(error) {
|
|
3306
4499
|
return error instanceof Error ? error.message : String(error);
|
|
3307
4500
|
}
|
|
3308
4501
|
|
|
@@ -3310,7 +4503,7 @@ function describe3(error) {
|
|
|
3310
4503
|
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
3311
4504
|
import { readdir as readdir2 } from "node:fs/promises";
|
|
3312
4505
|
import os2 from "node:os";
|
|
3313
|
-
import
|
|
4506
|
+
import path11 from "node:path";
|
|
3314
4507
|
import { listSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
3315
4508
|
var CACHE_TTL = 6e4;
|
|
3316
4509
|
var FIND_LIMIT = 50;
|
|
@@ -3349,7 +4542,7 @@ var ProjectFinder = class {
|
|
|
3349
4542
|
if (needles.length === 0)
|
|
3350
4543
|
return { candidates: [], truncated: false };
|
|
3351
4544
|
const open = new Map(this.registry.list().map((workspace) => [workspace.path.toLowerCase(), workspace]));
|
|
3352
|
-
const byName = this.#dirs.filter((dir) => matches(
|
|
4545
|
+
const byName = this.#dirs.filter((dir) => matches(path11.basename(dir.path), needles));
|
|
3353
4546
|
const matched = byName.length > 0 ? byName : this.#dirs.filter((dir) => matches(dir.path, needles));
|
|
3354
4547
|
const sorted = [...matched].sort((a, b) => {
|
|
3355
4548
|
const aOpen = open.has(a.path.toLowerCase());
|
|
@@ -3360,7 +4553,7 @@ var ProjectFinder = class {
|
|
|
3360
4553
|
return a.sessions > 0 ? -1 : 1;
|
|
3361
4554
|
if (a.lastUsedAt !== b.lastUsedAt)
|
|
3362
4555
|
return b.lastUsedAt - a.lastUsedAt;
|
|
3363
|
-
return
|
|
4556
|
+
return path11.basename(a.path).localeCompare(path11.basename(b.path), void 0, {
|
|
3364
4557
|
numeric: true,
|
|
3365
4558
|
sensitivity: "base"
|
|
3366
4559
|
});
|
|
@@ -3368,7 +4561,7 @@ var ProjectFinder = class {
|
|
|
3368
4561
|
return {
|
|
3369
4562
|
candidates: sorted.slice(0, FIND_LIMIT).map((dir) => ({
|
|
3370
4563
|
path: dir.path,
|
|
3371
|
-
name:
|
|
4564
|
+
name: path11.basename(dir.path) || dir.path,
|
|
3372
4565
|
sessions: dir.sessions,
|
|
3373
4566
|
lastUsedAt: dir.lastUsedAt || null,
|
|
3374
4567
|
open: open.has(dir.path.toLowerCase()),
|
|
@@ -3392,7 +4585,7 @@ var ProjectFinder = class {
|
|
|
3392
4585
|
const candidates = [];
|
|
3393
4586
|
for (const entry of named) {
|
|
3394
4587
|
const absolute = entry.path.trim();
|
|
3395
|
-
if (!absolute || !
|
|
4588
|
+
if (!absolute || !path11.isAbsolute(absolute))
|
|
3396
4589
|
continue;
|
|
3397
4590
|
const key = absolute.toLowerCase();
|
|
3398
4591
|
if (seen.has(key))
|
|
@@ -3403,7 +4596,7 @@ var ProjectFinder = class {
|
|
|
3403
4596
|
const record = known.get(key);
|
|
3404
4597
|
candidates.push({
|
|
3405
4598
|
path: record?.path ?? absolute,
|
|
3406
|
-
name:
|
|
4599
|
+
name: path11.basename(absolute) || absolute,
|
|
3407
4600
|
sessions: record?.sessions ?? 0,
|
|
3408
4601
|
lastUsedAt: record?.lastUsedAt || null,
|
|
3409
4602
|
open: open.has(key),
|
|
@@ -3460,7 +4653,7 @@ var ProjectFinder = class {
|
|
|
3460
4653
|
if (!dirent.isDirectory() || SKIP_NAMES.has(dirent.name) || dirent.name.startsWith(".")) {
|
|
3461
4654
|
continue;
|
|
3462
4655
|
}
|
|
3463
|
-
const full =
|
|
4656
|
+
const full = path11.join(parent, dirent.name);
|
|
3464
4657
|
const key = full.toLowerCase();
|
|
3465
4658
|
if (dirs.has(key))
|
|
3466
4659
|
continue;
|
|
@@ -3481,7 +4674,7 @@ var ProjectFinder = class {
|
|
|
3481
4674
|
#parents(dirs) {
|
|
3482
4675
|
const counted = /* @__PURE__ */ new Map();
|
|
3483
4676
|
for (const dir of dirs) {
|
|
3484
|
-
const parent =
|
|
4677
|
+
const parent = path11.dirname(dir.path);
|
|
3485
4678
|
if (parent === dir.path || !usable(parent))
|
|
3486
4679
|
continue;
|
|
3487
4680
|
const key = parent.toLowerCase();
|
|
@@ -3593,18 +4786,18 @@ function fromText(text) {
|
|
|
3593
4786
|
|
|
3594
4787
|
// ../host/dist/workspaces.js
|
|
3595
4788
|
import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
|
|
3596
|
-
import { mkdir as
|
|
3597
|
-
import
|
|
4789
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
|
|
4790
|
+
import path12 from "node:path";
|
|
3598
4791
|
import { listSessions as listSessions2 } from "@anthropic-ai/claude-agent-sdk";
|
|
3599
4792
|
var WorkspaceRegistry = class {
|
|
3600
4793
|
#entries = [];
|
|
3601
4794
|
#storePath;
|
|
3602
4795
|
constructor(dataDir) {
|
|
3603
|
-
this.#storePath =
|
|
4796
|
+
this.#storePath = path12.join(dataDir, "workspaces.json");
|
|
3604
4797
|
}
|
|
3605
4798
|
async load() {
|
|
3606
4799
|
try {
|
|
3607
|
-
const raw = await
|
|
4800
|
+
const raw = await readFile7(this.#storePath, "utf8");
|
|
3608
4801
|
const parsed = JSON.parse(raw);
|
|
3609
4802
|
if (!Array.isArray(parsed)) {
|
|
3610
4803
|
this.#entries = [];
|
|
@@ -3622,8 +4815,8 @@ var WorkspaceRegistry = class {
|
|
|
3622
4815
|
}
|
|
3623
4816
|
}
|
|
3624
4817
|
async #persist() {
|
|
3625
|
-
await
|
|
3626
|
-
await
|
|
4818
|
+
await mkdir5(path12.dirname(this.#storePath), { recursive: true });
|
|
4819
|
+
await writeFile6(this.#storePath, JSON.stringify(this.#entries, null, 2), "utf8");
|
|
3627
4820
|
}
|
|
3628
4821
|
/** Активные проекты. */
|
|
3629
4822
|
list() {
|
|
@@ -3638,7 +4831,7 @@ var WorkspaceRegistry = class {
|
|
|
3638
4831
|
return entry ? toInfo(entry) : void 0;
|
|
3639
4832
|
}
|
|
3640
4833
|
async open(rawPath) {
|
|
3641
|
-
const absolute =
|
|
4834
|
+
const absolute = path12.resolve(rawPath);
|
|
3642
4835
|
if (!existsSync3(absolute)) {
|
|
3643
4836
|
throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${absolute}`);
|
|
3644
4837
|
}
|
|
@@ -3705,24 +4898,24 @@ var WorkspaceRegistry = class {
|
|
|
3705
4898
|
async create(parentRaw, nameRaw, init) {
|
|
3706
4899
|
const name = nameRaw.trim();
|
|
3707
4900
|
requireName(name);
|
|
3708
|
-
const parent =
|
|
4901
|
+
const parent = path12.resolve(parentRaw.trim());
|
|
3709
4902
|
if (!existsSync3(parent)) {
|
|
3710
4903
|
throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${parent}`);
|
|
3711
4904
|
}
|
|
3712
4905
|
if (!statSync2(parent).isDirectory()) {
|
|
3713
4906
|
throw new Error(`\u042D\u0442\u043E \u043D\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433: ${parent}`);
|
|
3714
4907
|
}
|
|
3715
|
-
const target =
|
|
4908
|
+
const target = path12.join(parent, name);
|
|
3716
4909
|
if (existsSync3(target)) {
|
|
3717
4910
|
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
4911
|
}
|
|
3719
|
-
await
|
|
4912
|
+
await mkdir5(target);
|
|
3720
4913
|
const warnings = [];
|
|
3721
4914
|
if (init.claudeMd) {
|
|
3722
|
-
await step(warnings, "CLAUDE.md", () =>
|
|
4915
|
+
await step(warnings, "CLAUDE.md", () => writeFile6(path12.join(target, "CLAUDE.md"), claudeMdStub(name), "utf8"));
|
|
3723
4916
|
}
|
|
3724
4917
|
if (init.readme) {
|
|
3725
|
-
await step(warnings, "README.md", () =>
|
|
4918
|
+
await step(warnings, "README.md", () => writeFile6(path12.join(target, "README.md"), readmeStub(name), "utf8"));
|
|
3726
4919
|
}
|
|
3727
4920
|
if (init.git) {
|
|
3728
4921
|
await step(warnings, "git init", () => new GitRepo(target).init());
|
|
@@ -3794,8 +4987,8 @@ function toInfo(entry) {
|
|
|
3794
4987
|
return {
|
|
3795
4988
|
id: workspaceIdFromPath(absolute),
|
|
3796
4989
|
path: absolute,
|
|
3797
|
-
name:
|
|
3798
|
-
hasClaudeMd: existsSync3(
|
|
4990
|
+
name: path12.basename(absolute) || absolute,
|
|
4991
|
+
hasClaudeMd: existsSync3(path12.join(absolute, "CLAUDE.md")),
|
|
3799
4992
|
...archivedAt ? { archivedAt } : {}
|
|
3800
4993
|
};
|
|
3801
4994
|
}
|
|
@@ -3804,18 +4997,18 @@ function isEntry(value) {
|
|
|
3804
4997
|
}
|
|
3805
4998
|
|
|
3806
4999
|
// ../host/dist/archive.js
|
|
3807
|
-
import { mkdir as
|
|
3808
|
-
import
|
|
5000
|
+
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
|
|
5001
|
+
import path13 from "node:path";
|
|
3809
5002
|
var SessionArchive = class {
|
|
3810
5003
|
#archived = /* @__PURE__ */ new Map();
|
|
3811
5004
|
#restored = /* @__PURE__ */ new Map();
|
|
3812
5005
|
#storePath;
|
|
3813
5006
|
constructor(dataDir) {
|
|
3814
|
-
this.#storePath =
|
|
5007
|
+
this.#storePath = path13.join(dataDir, "session-archive.json");
|
|
3815
5008
|
}
|
|
3816
5009
|
async load() {
|
|
3817
5010
|
try {
|
|
3818
|
-
const raw = await
|
|
5011
|
+
const raw = await readFile8(this.#storePath, "utf8");
|
|
3819
5012
|
const parsed = JSON.parse(raw);
|
|
3820
5013
|
this.#archived = new Map(Object.entries(parsed.archived ?? {}).filter(([, r]) => isRecord(r)));
|
|
3821
5014
|
this.#restored = new Map(Object.entries(parsed.restored ?? {}).filter((entry) => typeof entry[1] === "number"));
|
|
@@ -3904,8 +5097,8 @@ var SessionArchive = class {
|
|
|
3904
5097
|
archived: Object.fromEntries(this.#archived),
|
|
3905
5098
|
restored: Object.fromEntries(this.#restored)
|
|
3906
5099
|
};
|
|
3907
|
-
await
|
|
3908
|
-
await
|
|
5100
|
+
await mkdir6(path13.dirname(this.#storePath), { recursive: true });
|
|
5101
|
+
await writeFile7(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
3909
5102
|
}
|
|
3910
5103
|
};
|
|
3911
5104
|
function isRecord(value) {
|
|
@@ -3913,17 +5106,17 @@ function isRecord(value) {
|
|
|
3913
5106
|
}
|
|
3914
5107
|
|
|
3915
5108
|
// ../host/dist/pins.js
|
|
3916
|
-
import { mkdir as
|
|
3917
|
-
import
|
|
5109
|
+
import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
|
|
5110
|
+
import path14 from "node:path";
|
|
3918
5111
|
var SessionPins = class {
|
|
3919
5112
|
#pinned = /* @__PURE__ */ new Map();
|
|
3920
5113
|
#storePath;
|
|
3921
5114
|
constructor(dataDir) {
|
|
3922
|
-
this.#storePath =
|
|
5115
|
+
this.#storePath = path14.join(dataDir, "session-pins.json");
|
|
3923
5116
|
}
|
|
3924
5117
|
async load() {
|
|
3925
5118
|
try {
|
|
3926
|
-
const raw = await
|
|
5119
|
+
const raw = await readFile9(this.#storePath, "utf8");
|
|
3927
5120
|
const parsed = JSON.parse(raw);
|
|
3928
5121
|
this.#pinned = new Map(Object.entries(parsed.pinned ?? {}).filter(([, r]) => isRecord2(r)));
|
|
3929
5122
|
} catch {
|
|
@@ -3982,6 +5175,28 @@ var SessionPins = class {
|
|
|
3982
5175
|
await this.#persist();
|
|
3983
5176
|
return true;
|
|
3984
5177
|
}
|
|
5178
|
+
/**
|
|
5179
|
+
* Передать закрепление другой сессии — «Продолжить в новой сессии» из
|
|
5180
|
+
* хэндоффа: работа переехала в новую, и закреплённой должна быть она.
|
|
5181
|
+
*
|
|
5182
|
+
* `fromIds` — все id прежней сессии (дисковый и, если она ещё жива,
|
|
5183
|
+
* локальный uuid): закрепление пишется под оба, и снять его надо с обоих,
|
|
5184
|
+
* иначе прежняя так и останется в панели. Время берётся у первой найденной
|
|
5185
|
+
* записи — новая встаёт ровно на место прежней, а не в конец панели.
|
|
5186
|
+
* Возвращает `true`, если прежняя была закреплена.
|
|
5187
|
+
*/
|
|
5188
|
+
async transfer(fromIds, toId) {
|
|
5189
|
+
let record;
|
|
5190
|
+
for (const id of fromIds) {
|
|
5191
|
+
record ??= this.#pinned.get(id);
|
|
5192
|
+
this.#pinned.delete(id);
|
|
5193
|
+
}
|
|
5194
|
+
if (!record)
|
|
5195
|
+
return false;
|
|
5196
|
+
this.#pinned.set(toId, record);
|
|
5197
|
+
await this.#persist();
|
|
5198
|
+
return true;
|
|
5199
|
+
}
|
|
3985
5200
|
/**
|
|
3986
5201
|
* Забыть записи о сессиях, которых больше нет на диске.
|
|
3987
5202
|
*
|
|
@@ -4003,8 +5218,8 @@ var SessionPins = class {
|
|
|
4003
5218
|
}
|
|
4004
5219
|
async #persist() {
|
|
4005
5220
|
const file = { pinned: Object.fromEntries(this.#pinned) };
|
|
4006
|
-
await
|
|
4007
|
-
await
|
|
5221
|
+
await mkdir7(path14.dirname(this.#storePath), { recursive: true });
|
|
5222
|
+
await writeFile8(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
4008
5223
|
}
|
|
4009
5224
|
};
|
|
4010
5225
|
function isRecord2(value) {
|
|
@@ -4012,17 +5227,17 @@ function isRecord2(value) {
|
|
|
4012
5227
|
}
|
|
4013
5228
|
|
|
4014
5229
|
// ../host/dist/order.js
|
|
4015
|
-
import { mkdir as
|
|
4016
|
-
import
|
|
5230
|
+
import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile9 } from "node:fs/promises";
|
|
5231
|
+
import path15 from "node:path";
|
|
4017
5232
|
var ListOrderStore = class {
|
|
4018
5233
|
#order = emptyOrder();
|
|
4019
5234
|
#storePath;
|
|
4020
5235
|
constructor(dataDir) {
|
|
4021
|
-
this.#storePath =
|
|
5236
|
+
this.#storePath = path15.join(dataDir, "list-order.json");
|
|
4022
5237
|
}
|
|
4023
5238
|
async load() {
|
|
4024
5239
|
try {
|
|
4025
|
-
const raw = await
|
|
5240
|
+
const raw = await readFile10(this.#storePath, "utf8");
|
|
4026
5241
|
this.#order = sanitize(JSON.parse(raw));
|
|
4027
5242
|
} catch {
|
|
4028
5243
|
this.#order = emptyOrder();
|
|
@@ -4052,8 +5267,8 @@ var ListOrderStore = class {
|
|
|
4052
5267
|
} else {
|
|
4053
5268
|
this.#order[scope] = clean ?? [];
|
|
4054
5269
|
}
|
|
4055
|
-
await
|
|
4056
|
-
await
|
|
5270
|
+
await mkdir8(path15.dirname(this.#storePath), { recursive: true });
|
|
5271
|
+
await writeFile9(this.#storePath, JSON.stringify(this.#order, null, 2), "utf8");
|
|
4057
5272
|
}
|
|
4058
5273
|
};
|
|
4059
5274
|
function emptyOrder() {
|
|
@@ -4082,9 +5297,9 @@ function sanitize(value) {
|
|
|
4082
5297
|
}
|
|
4083
5298
|
|
|
4084
5299
|
// ../host/dist/folders.js
|
|
4085
|
-
import { mkdir as
|
|
4086
|
-
import
|
|
4087
|
-
import { randomUUID as
|
|
5300
|
+
import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile10 } from "node:fs/promises";
|
|
5301
|
+
import path16 from "node:path";
|
|
5302
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
4088
5303
|
var Folders = class {
|
|
4089
5304
|
#workspaceFolders = /* @__PURE__ */ new Map();
|
|
4090
5305
|
#sessionFolders = /* @__PURE__ */ new Map();
|
|
@@ -4094,11 +5309,11 @@ var Folders = class {
|
|
|
4094
5309
|
#pinnedAssignments = /* @__PURE__ */ new Map();
|
|
4095
5310
|
#storePath;
|
|
4096
5311
|
constructor(dataDir) {
|
|
4097
|
-
this.#storePath =
|
|
5312
|
+
this.#storePath = path16.join(dataDir, "folders.json");
|
|
4098
5313
|
}
|
|
4099
5314
|
async load() {
|
|
4100
5315
|
try {
|
|
4101
|
-
const raw = await
|
|
5316
|
+
const raw = await readFile11(this.#storePath, "utf8");
|
|
4102
5317
|
const parsed = JSON.parse(raw);
|
|
4103
5318
|
this.#workspaceFolders = new Map(Object.entries(parsed.workspaceFolders ?? {}).filter(([, r]) => isWorkspaceFolder(r)));
|
|
4104
5319
|
this.#sessionFolders = new Map(Object.entries(parsed.sessionFolders ?? {}).filter(([, r]) => isSessionFolder(r)));
|
|
@@ -4128,19 +5343,19 @@ var Folders = class {
|
|
|
4128
5343
|
};
|
|
4129
5344
|
}
|
|
4130
5345
|
async createWorkspaceFolder(name) {
|
|
4131
|
-
const id =
|
|
5346
|
+
const id = randomUUID3();
|
|
4132
5347
|
this.#workspaceFolders.set(id, { name });
|
|
4133
5348
|
await this.#persist();
|
|
4134
5349
|
return id;
|
|
4135
5350
|
}
|
|
4136
5351
|
async createSessionFolder(workspaceId, name) {
|
|
4137
|
-
const id =
|
|
5352
|
+
const id = randomUUID3();
|
|
4138
5353
|
this.#sessionFolders.set(id, { name, workspaceId });
|
|
4139
5354
|
await this.#persist();
|
|
4140
5355
|
return id;
|
|
4141
5356
|
}
|
|
4142
5357
|
async createPinnedFolder(name) {
|
|
4143
|
-
const id =
|
|
5358
|
+
const id = randomUUID3();
|
|
4144
5359
|
this.#pinnedFolders.set(id, { name });
|
|
4145
5360
|
await this.#persist();
|
|
4146
5361
|
return id;
|
|
@@ -4263,6 +5478,21 @@ var Folders = class {
|
|
|
4263
5478
|
await this.#persist();
|
|
4264
5479
|
return true;
|
|
4265
5480
|
}
|
|
5481
|
+
/**
|
|
5482
|
+
* Перенести место в папке закреплённых с одного id сессии на другой — вслед
|
|
5483
|
+
* за самим закреплением (см. `SessionPins.migrate` и `SessionPins.transfer`).
|
|
5484
|
+
* Без этого строка, переехав, выпала бы из своей папки в общий список.
|
|
5485
|
+
*/
|
|
5486
|
+
async migratePinnedSession(fromId, toId) {
|
|
5487
|
+
const folderId = this.#pinnedAssignments.get(fromId);
|
|
5488
|
+
if (!folderId || fromId === toId)
|
|
5489
|
+
return false;
|
|
5490
|
+
this.#pinnedAssignments.delete(fromId);
|
|
5491
|
+
if (!this.#pinnedAssignments.has(toId))
|
|
5492
|
+
this.#pinnedAssignments.set(toId, folderId);
|
|
5493
|
+
await this.#persist();
|
|
5494
|
+
return true;
|
|
5495
|
+
}
|
|
4266
5496
|
folderIdForWorkspace(workspaceId) {
|
|
4267
5497
|
return this.#workspaceAssignments.get(workspaceId);
|
|
4268
5498
|
}
|
|
@@ -4281,8 +5511,8 @@ var Folders = class {
|
|
|
4281
5511
|
sessionAssignments: Object.fromEntries(this.#sessionAssignments),
|
|
4282
5512
|
pinnedAssignments: Object.fromEntries(this.#pinnedAssignments)
|
|
4283
5513
|
};
|
|
4284
|
-
await
|
|
4285
|
-
await
|
|
5514
|
+
await mkdir9(path16.dirname(this.#storePath), { recursive: true });
|
|
5515
|
+
await writeFile10(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
4286
5516
|
}
|
|
4287
5517
|
};
|
|
4288
5518
|
function isWorkspaceFolder(value) {
|
|
@@ -4296,9 +5526,9 @@ function isPinnedFolder(value) {
|
|
|
4296
5526
|
}
|
|
4297
5527
|
|
|
4298
5528
|
// ../host/dist/app-settings.js
|
|
4299
|
-
import { mkdir as
|
|
5529
|
+
import { mkdir as mkdir10, readFile as readFile12, writeFile as writeFile11 } from "node:fs/promises";
|
|
4300
5530
|
import { homedir as homedir4 } from "node:os";
|
|
4301
|
-
import
|
|
5531
|
+
import path17 from "node:path";
|
|
4302
5532
|
var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
4303
5533
|
var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
|
|
4304
5534
|
var DEFAULT_AUTO_STOP_AFTER_MS = 10 * 60 * 1e3;
|
|
@@ -4332,17 +5562,22 @@ var DEFAULT_APP_SETTINGS = {
|
|
|
4332
5562
|
// машине что угодно.
|
|
4333
5563
|
terminalEnabled: false,
|
|
4334
5564
|
// Выключено по умолчанию: списки идут в том порядке, в каком их расставили.
|
|
4335
|
-
liveSessionsFirst: false
|
|
5565
|
+
liveSessionsFirst: false,
|
|
5566
|
+
// Пятичасовое окно останавливаем с запасом: хэндофф — это ещё один ход, и
|
|
5567
|
+
// ему нужно место. Недельное — у самого края: до сброса там может быть
|
|
5568
|
+
// несколько дней, и терять их из-за пары процентов жалко.
|
|
5569
|
+
limitStopFiveHourPct: 95,
|
|
5570
|
+
limitStopSevenDayPct: 99
|
|
4336
5571
|
};
|
|
4337
5572
|
var AppSettingsStore = class {
|
|
4338
5573
|
#settings = { ...DEFAULT_APP_SETTINGS };
|
|
4339
5574
|
#storePath;
|
|
4340
5575
|
constructor(dataDir) {
|
|
4341
|
-
this.#storePath =
|
|
5576
|
+
this.#storePath = path17.join(dataDir, "app-settings.json");
|
|
4342
5577
|
}
|
|
4343
5578
|
async load() {
|
|
4344
5579
|
try {
|
|
4345
|
-
const raw = await
|
|
5580
|
+
const raw = await readFile12(this.#storePath, "utf8");
|
|
4346
5581
|
this.#settings = sanitize2(JSON.parse(raw), DEFAULT_APP_SETTINGS);
|
|
4347
5582
|
} catch {
|
|
4348
5583
|
this.#settings = { ...DEFAULT_APP_SETTINGS };
|
|
@@ -4362,8 +5597,8 @@ var AppSettingsStore = class {
|
|
|
4362
5597
|
async update(patch) {
|
|
4363
5598
|
const next = sanitize2(patch, this.#settings);
|
|
4364
5599
|
this.#settings = next;
|
|
4365
|
-
await
|
|
4366
|
-
await
|
|
5600
|
+
await mkdir10(path17.dirname(this.#storePath), { recursive: true });
|
|
5601
|
+
await writeFile11(this.#storePath, JSON.stringify(next, null, 2), "utf8");
|
|
4367
5602
|
return this.current;
|
|
4368
5603
|
}
|
|
4369
5604
|
};
|
|
@@ -4405,10 +5640,16 @@ function sanitize2(value, fallback) {
|
|
|
4405
5640
|
if (typeof record.liveSessionsFirst === "boolean") {
|
|
4406
5641
|
result.liveSessionsFirst = record.liveSessionsFirst;
|
|
4407
5642
|
}
|
|
5643
|
+
for (const key of ["limitStopFiveHourPct", "limitStopSevenDayPct"]) {
|
|
5644
|
+
const pct = record[key];
|
|
5645
|
+
if (typeof pct === "number" && Number.isFinite(pct)) {
|
|
5646
|
+
result[key] = pct <= 0 ? 0 : Math.min(Math.max(Math.round(pct), 1), 100);
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
4408
5649
|
return result;
|
|
4409
5650
|
}
|
|
4410
5651
|
function claudeHome() {
|
|
4411
|
-
return
|
|
5652
|
+
return path17.join(homedir4(), ".claude");
|
|
4412
5653
|
}
|
|
4413
5654
|
function writableRoots(mode, cwd, workspacePaths) {
|
|
4414
5655
|
if (mode === "all")
|
|
@@ -4418,17 +5659,189 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
4418
5659
|
return [...roots];
|
|
4419
5660
|
for (const workspace of [cwd, ...workspacePaths]) {
|
|
4420
5661
|
roots.add(workspace);
|
|
4421
|
-
const parent =
|
|
5662
|
+
const parent = path17.dirname(workspace);
|
|
4422
5663
|
if (parent !== workspace)
|
|
4423
5664
|
roots.add(parent);
|
|
4424
5665
|
}
|
|
4425
5666
|
return [...roots];
|
|
4426
5667
|
}
|
|
4427
5668
|
|
|
5669
|
+
// ../host/dist/handoff-store.js
|
|
5670
|
+
import { mkdir as mkdir11, readFile as readFile13, readdir as readdir3, rm as rm4, writeFile as writeFile12 } from "node:fs/promises";
|
|
5671
|
+
import path18 from "node:path";
|
|
5672
|
+
var HandoffStore = class {
|
|
5673
|
+
#dir;
|
|
5674
|
+
constructor(dataDir) {
|
|
5675
|
+
this.#dir = path18.join(dataDir, "handoffs");
|
|
5676
|
+
}
|
|
5677
|
+
async get(sessionId) {
|
|
5678
|
+
const file = this.#file(sessionId);
|
|
5679
|
+
if (!file)
|
|
5680
|
+
return null;
|
|
5681
|
+
try {
|
|
5682
|
+
const parsed = JSON.parse(await readFile13(file, "utf8"));
|
|
5683
|
+
return typeof parsed.text === "string" ? parsed : null;
|
|
5684
|
+
} catch {
|
|
5685
|
+
return null;
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
async save(handoff) {
|
|
5689
|
+
const file = this.#file(handoff.sessionId);
|
|
5690
|
+
if (!file)
|
|
5691
|
+
return;
|
|
5692
|
+
await mkdir11(this.#dir, { recursive: true });
|
|
5693
|
+
await writeFile12(file, JSON.stringify(handoff, null, 2), "utf8");
|
|
5694
|
+
}
|
|
5695
|
+
async remove(sessionId) {
|
|
5696
|
+
const file = this.#file(sessionId);
|
|
5697
|
+
if (file)
|
|
5698
|
+
await rm4(file, { force: true });
|
|
5699
|
+
}
|
|
5700
|
+
/** id всех сохранённых хэндоффов. */
|
|
5701
|
+
async list() {
|
|
5702
|
+
try {
|
|
5703
|
+
return (await readdir3(this.#dir)).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length));
|
|
5704
|
+
} catch {
|
|
5705
|
+
return [];
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5708
|
+
/** Путь файла; `null` — id пришёл от клиента и на имя файла не годится. */
|
|
5709
|
+
#file(sessionId) {
|
|
5710
|
+
if (!/^[\w-]{1,128}$/.test(sessionId))
|
|
5711
|
+
return null;
|
|
5712
|
+
return path18.join(this.#dir, `${sessionId}.json`);
|
|
5713
|
+
}
|
|
5714
|
+
};
|
|
5715
|
+
var LimitStopState = class {
|
|
5716
|
+
#fired = {};
|
|
5717
|
+
#storePath;
|
|
5718
|
+
constructor(dataDir) {
|
|
5719
|
+
this.#storePath = path18.join(dataDir, "limit-stop.json");
|
|
5720
|
+
}
|
|
5721
|
+
async load() {
|
|
5722
|
+
try {
|
|
5723
|
+
const parsed = JSON.parse(await readFile13(this.#storePath, "utf8"));
|
|
5724
|
+
this.#fired = {};
|
|
5725
|
+
for (const key of ["fiveHour", "sevenDay"]) {
|
|
5726
|
+
const until = parsed[key];
|
|
5727
|
+
if (typeof until === "number" && Number.isFinite(until))
|
|
5728
|
+
this.#fired[key] = until;
|
|
5729
|
+
}
|
|
5730
|
+
} catch {
|
|
5731
|
+
this.#fired = {};
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
/** Окно `limit` уже сработало и ещё не сбросилось. */
|
|
5735
|
+
fired(limit, now = Date.now()) {
|
|
5736
|
+
return (this.#fired[limit] ?? 0) > now;
|
|
5737
|
+
}
|
|
5738
|
+
/** Запомнить срабатывание до `until` (момент сброса окна). */
|
|
5739
|
+
async mark(limit, until) {
|
|
5740
|
+
this.#fired[limit] = until;
|
|
5741
|
+
await mkdir11(path18.dirname(this.#storePath), { recursive: true });
|
|
5742
|
+
await writeFile12(this.#storePath, JSON.stringify(this.#fired, null, 2), "utf8");
|
|
5743
|
+
}
|
|
5744
|
+
};
|
|
5745
|
+
|
|
5746
|
+
// ../host/dist/limit-guard.js
|
|
5747
|
+
var HANDOFF_TIMEOUT_MS = 3 * 6e4;
|
|
5748
|
+
var LIMIT_SWEEP_MS = 6e4;
|
|
5749
|
+
var WINDOW_SPAN_MS = {
|
|
5750
|
+
fiveHour: 5 * 60 * 6e4,
|
|
5751
|
+
sevenDay: 7 * 24 * 60 * 6e4
|
|
5752
|
+
};
|
|
5753
|
+
var LIMIT_LABEL = {
|
|
5754
|
+
fiveHour: "5-\u0447\u0430\u0441\u043E\u0432\u043E\u0439 \u043B\u0438\u043C\u0438\u0442",
|
|
5755
|
+
sevenDay: "\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043B\u0438\u043C\u0438\u0442"
|
|
5756
|
+
};
|
|
5757
|
+
function crossedLimit(usage, settings, fired, now = Date.now()) {
|
|
5758
|
+
if (!usage.available)
|
|
5759
|
+
return null;
|
|
5760
|
+
const checks = [
|
|
5761
|
+
["fiveHour", usage.fiveHour, settings.limitStopFiveHourPct],
|
|
5762
|
+
["sevenDay", usage.sevenDay, settings.limitStopSevenDayPct]
|
|
5763
|
+
];
|
|
5764
|
+
for (const [limit, window, threshold] of checks) {
|
|
5765
|
+
if (!window || threshold <= 0 || window.utilization < threshold)
|
|
5766
|
+
continue;
|
|
5767
|
+
if (fired(limit))
|
|
5768
|
+
continue;
|
|
5769
|
+
const resets = Date.parse(window.resetsAt);
|
|
5770
|
+
const until = Number.isFinite(resets) && resets > now ? resets : now + WINDOW_SPAN_MS[limit];
|
|
5771
|
+
return { limit, window, until };
|
|
5772
|
+
}
|
|
5773
|
+
return null;
|
|
5774
|
+
}
|
|
5775
|
+
function handoffPrompt(hit) {
|
|
5776
|
+
return [
|
|
5777
|
+
hit ? `[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.` : "[ccv] \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u044D\u0442\u043E\u0442 \u044D\u0442\u0430\u043F: \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E\u0439\u0434\u0451\u0442 \u0432 \u043D\u043E\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438.",
|
|
5778
|
+
"\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.",
|
|
5779
|
+
"\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.",
|
|
5780
|
+
"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),",
|
|
5781
|
+
"\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.",
|
|
5782
|
+
"\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."
|
|
5783
|
+
].join("\n");
|
|
5784
|
+
}
|
|
5785
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
5786
|
+
function journalHandoff(entries) {
|
|
5787
|
+
const prompts = [];
|
|
5788
|
+
let lastText = "";
|
|
5789
|
+
let todos = null;
|
|
5790
|
+
const files = /* @__PURE__ */ new Set();
|
|
5791
|
+
for (const { event } of entries) {
|
|
5792
|
+
if (event.kind === "user.message") {
|
|
5793
|
+
if (!event.text.startsWith("[ccv]"))
|
|
5794
|
+
prompts.push(event.text);
|
|
5795
|
+
} else if (event.kind === "assistant.text") {
|
|
5796
|
+
if (event.text.trim())
|
|
5797
|
+
lastText = event.text;
|
|
5798
|
+
} else if (event.kind === "tool.use") {
|
|
5799
|
+
const input = event.input ?? {};
|
|
5800
|
+
if (EDIT_TOOLS.has(event.name)) {
|
|
5801
|
+
const target = input.file_path ?? input.notebook_path;
|
|
5802
|
+
if (typeof target === "string")
|
|
5803
|
+
files.add(target);
|
|
5804
|
+
} else if (event.name === "TodoWrite" && Array.isArray(input.todos)) {
|
|
5805
|
+
todos = input.todos.flatMap((todo) => {
|
|
5806
|
+
const t = todo;
|
|
5807
|
+
return typeof t.content === "string" ? [{ content: t.content, status: String(t.status ?? "") }] : [];
|
|
5808
|
+
});
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
const sections = [
|
|
5813
|
+
"_\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._"
|
|
5814
|
+
];
|
|
5815
|
+
if (prompts.length > 0) {
|
|
5816
|
+
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"));
|
|
5817
|
+
}
|
|
5818
|
+
if (todos && todos.length > 0) {
|
|
5819
|
+
const mark = (status) => status === "completed" ? "[x]" : status === "in_progress" ? "[~]" : "[ ]";
|
|
5820
|
+
sections.push("## \u0417\u0430\u0434\u0430\u0447\u0438\n\n" + todos.map((t) => `- ${mark(t.status)} ${t.content}`).join("\n"));
|
|
5821
|
+
}
|
|
5822
|
+
if (files.size > 0) {
|
|
5823
|
+
sections.push("## \u0422\u0440\u043E\u043D\u0443\u0442\u044B\u0435 \u0444\u0430\u0439\u043B\u044B\n\n" + [...files].map((f) => `- \`${f}\``).join("\n"));
|
|
5824
|
+
}
|
|
5825
|
+
if (lastText)
|
|
5826
|
+
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));
|
|
5827
|
+
if (sections.length === 1)
|
|
5828
|
+
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.");
|
|
5829
|
+
return sections.join("\n\n");
|
|
5830
|
+
}
|
|
5831
|
+
function clip2(text, max) {
|
|
5832
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
5833
|
+
}
|
|
5834
|
+
function quote(text) {
|
|
5835
|
+
return text.split("\n").map((line) => `> ${line}`).join("\n");
|
|
5836
|
+
}
|
|
5837
|
+
function capitalize(text) {
|
|
5838
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
5839
|
+
}
|
|
5840
|
+
|
|
4428
5841
|
// ../host/dist/plugins.js
|
|
4429
|
-
import { readFile as
|
|
5842
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
4430
5843
|
import { homedir as homedir5 } from "node:os";
|
|
4431
|
-
import
|
|
5844
|
+
import path19 from "node:path";
|
|
4432
5845
|
var TIMEOUT_MS2 = 12e4;
|
|
4433
5846
|
var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
|
|
4434
5847
|
var PluginCli = class {
|
|
@@ -4527,10 +5940,10 @@ async function readCatalog(markets, installed) {
|
|
|
4527
5940
|
return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
4528
5941
|
}
|
|
4529
5942
|
async function readMarketplace(market, installed) {
|
|
4530
|
-
const root = market.installLocation ||
|
|
5943
|
+
const root = market.installLocation || path19.join(homedir5(), ".claude", "plugins", "marketplaces", market.name);
|
|
4531
5944
|
let manifest;
|
|
4532
5945
|
try {
|
|
4533
|
-
const text = await
|
|
5946
|
+
const text = await readFile14(path19.join(root, ".claude-plugin", "marketplace.json"), "utf8");
|
|
4534
5947
|
manifest = JSON.parse(text);
|
|
4535
5948
|
} catch {
|
|
4536
5949
|
return [];
|
|
@@ -4573,7 +5986,7 @@ function authorName(raw) {
|
|
|
4573
5986
|
}
|
|
4574
5987
|
|
|
4575
5988
|
// ../host/dist/auth.js
|
|
4576
|
-
import { EventEmitter as
|
|
5989
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
4577
5990
|
var QUICK_TIMEOUT_MS = 3e4;
|
|
4578
5991
|
var LOGIN_TIMEOUT_MS = 10 * 6e4;
|
|
4579
5992
|
var URL_RE = /https:\/\/\S*\/oauth\/authorize\S*/;
|
|
@@ -4585,7 +5998,7 @@ var IDLE = {
|
|
|
4585
5998
|
output: "",
|
|
4586
5999
|
error: null
|
|
4587
6000
|
};
|
|
4588
|
-
var AuthCli = class extends
|
|
6001
|
+
var AuthCli = class extends EventEmitter3 {
|
|
4589
6002
|
#child = null;
|
|
4590
6003
|
#state = IDLE;
|
|
4591
6004
|
#timer = null;
|
|
@@ -4617,7 +6030,7 @@ var AuthCli = class extends EventEmitter2 {
|
|
|
4617
6030
|
if (this.#child)
|
|
4618
6031
|
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");
|
|
4619
6032
|
if (switchAccount) {
|
|
4620
|
-
const out = await run(["auth", "logout"], QUICK_TIMEOUT_MS).catch(
|
|
6033
|
+
const out = await run(["auth", "logout"], QUICK_TIMEOUT_MS).catch(describe5);
|
|
4621
6034
|
this.#push({ ...IDLE, active: true, output: `${out}
|
|
4622
6035
|
` });
|
|
4623
6036
|
this.emit("changed");
|
|
@@ -4637,7 +6050,7 @@ var AuthCli = class extends EventEmitter2 {
|
|
|
4637
6050
|
child.stdout?.on("data", (chunk) => this.#absorb(chunk.toString()));
|
|
4638
6051
|
child.stderr?.on("data", (chunk) => this.#absorb(chunk.toString()));
|
|
4639
6052
|
child.on("error", (error) => {
|
|
4640
|
-
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" :
|
|
6053
|
+
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));
|
|
4641
6054
|
});
|
|
4642
6055
|
child.on("close", (code) => {
|
|
4643
6056
|
if (this.#child !== child)
|
|
@@ -4765,14 +6178,14 @@ function parseStatus2(raw) {
|
|
|
4765
6178
|
function str2(value) {
|
|
4766
6179
|
return typeof value === "string" ? value : "";
|
|
4767
6180
|
}
|
|
4768
|
-
function
|
|
6181
|
+
function describe5(error) {
|
|
4769
6182
|
return error instanceof Error ? error.message : String(error);
|
|
4770
6183
|
}
|
|
4771
6184
|
|
|
4772
6185
|
// ../host/dist/relay-link.js
|
|
4773
|
-
import { EventEmitter as
|
|
4774
|
-
import { readFile as
|
|
4775
|
-
import
|
|
6186
|
+
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
6187
|
+
import { readFile as readFile15, writeFile as writeFile13, rm as rm5, mkdir as mkdir12 } from "node:fs/promises";
|
|
6188
|
+
import path20 from "node:path";
|
|
4776
6189
|
import { hostname } from "node:os";
|
|
4777
6190
|
import WebSocket from "ws";
|
|
4778
6191
|
|
|
@@ -4789,7 +6202,7 @@ var SILENCE_TIMEOUT_MS = 95e3;
|
|
|
4789
6202
|
function jitter(delay) {
|
|
4790
6203
|
return Math.round(delay * (0.8 + Math.random() * 0.4));
|
|
4791
6204
|
}
|
|
4792
|
-
var RelayLink = class extends
|
|
6205
|
+
var RelayLink = class extends EventEmitter4 {
|
|
4793
6206
|
#credentials = null;
|
|
4794
6207
|
#socket = null;
|
|
4795
6208
|
#retry = RETRY_MIN_MS;
|
|
@@ -4827,7 +6240,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4827
6240
|
#log;
|
|
4828
6241
|
constructor(options2) {
|
|
4829
6242
|
super();
|
|
4830
|
-
this.#file =
|
|
6243
|
+
this.#file = path20.join(options2.dataDir, "relay.json");
|
|
4831
6244
|
this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
|
|
4832
6245
|
this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
|
|
4833
6246
|
}
|
|
@@ -4908,7 +6321,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4908
6321
|
}
|
|
4909
6322
|
async #read() {
|
|
4910
6323
|
try {
|
|
4911
|
-
const raw = await
|
|
6324
|
+
const raw = await readFile15(this.#file, "utf8");
|
|
4912
6325
|
const parsed = JSON.parse(raw);
|
|
4913
6326
|
if (typeof parsed !== "object" || parsed === null)
|
|
4914
6327
|
return null;
|
|
@@ -4921,8 +6334,8 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4921
6334
|
}
|
|
4922
6335
|
}
|
|
4923
6336
|
async #write(credentials) {
|
|
4924
|
-
await
|
|
4925
|
-
await
|
|
6337
|
+
await mkdir12(path20.dirname(this.#file), { recursive: true });
|
|
6338
|
+
await writeFile13(this.#file, `${JSON.stringify(credentials, null, 2)}
|
|
4926
6339
|
`, {
|
|
4927
6340
|
encoding: "utf8",
|
|
4928
6341
|
mode: 384
|
|
@@ -4946,7 +6359,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4946
6359
|
body: JSON.stringify({ code, name: machine, platform: process.platform })
|
|
4947
6360
|
});
|
|
4948
6361
|
} catch (error) {
|
|
4949
|
-
throw new Error(`\u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u0434\u043E ${url2}: ${
|
|
6362
|
+
throw new Error(`\u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u0434\u043E ${url2}: ${describe6(error)}`);
|
|
4950
6363
|
}
|
|
4951
6364
|
const data = await response.json().catch(() => ({}));
|
|
4952
6365
|
if (!response.ok || !data.token || !data.hostId) {
|
|
@@ -4974,7 +6387,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4974
6387
|
this.#error = null;
|
|
4975
6388
|
this.#clients.clear();
|
|
4976
6389
|
this.#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
|
|
4977
|
-
await
|
|
6390
|
+
await rm5(this.#file, { force: true });
|
|
4978
6391
|
this.#changed();
|
|
4979
6392
|
this.#log("\u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435 \u0441\u043D\u044F\u0442\u043E");
|
|
4980
6393
|
}
|
|
@@ -5165,11 +6578,11 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
5165
6578
|
socket.send(JSON.stringify(frame));
|
|
5166
6579
|
}
|
|
5167
6580
|
};
|
|
5168
|
-
function
|
|
6581
|
+
function describe6(error) {
|
|
5169
6582
|
return error instanceof Error ? error.message : String(error);
|
|
5170
6583
|
}
|
|
5171
6584
|
function humanize(error) {
|
|
5172
|
-
const message =
|
|
6585
|
+
const message = describe6(error);
|
|
5173
6586
|
if (/handshake has timed out/i.test(message)) {
|
|
5174
6587
|
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`;
|
|
5175
6588
|
}
|
|
@@ -5177,14 +6590,14 @@ function humanize(error) {
|
|
|
5177
6590
|
}
|
|
5178
6591
|
|
|
5179
6592
|
// ../host/dist/terminal.js
|
|
5180
|
-
import { EventEmitter as
|
|
6593
|
+
import { EventEmitter as EventEmitter5 } from "node:events";
|
|
5181
6594
|
import { access as access2, constants as constants2 } from "node:fs/promises";
|
|
5182
|
-
import
|
|
5183
|
-
import { randomUUID as
|
|
6595
|
+
import path21 from "node:path";
|
|
6596
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5184
6597
|
var SCROLLBACK_BYTES = 256 * 1024;
|
|
5185
6598
|
var FLUSH_MS = 12;
|
|
5186
6599
|
var MAX_TERMINALS = 24;
|
|
5187
|
-
var TerminalHub = class extends
|
|
6600
|
+
var TerminalHub = class extends EventEmitter5 {
|
|
5188
6601
|
#terminals = /* @__PURE__ */ new Map();
|
|
5189
6602
|
#module = null;
|
|
5190
6603
|
list(workspaceId) {
|
|
@@ -5217,7 +6630,7 @@ var TerminalHub = class extends EventEmitter4 {
|
|
|
5217
6630
|
env: environment()
|
|
5218
6631
|
});
|
|
5219
6632
|
const info = {
|
|
5220
|
-
id:
|
|
6633
|
+
id: randomUUID4(),
|
|
5221
6634
|
workspaceId: options2.workspaceId,
|
|
5222
6635
|
title: shell.title,
|
|
5223
6636
|
shell: shell.file,
|
|
@@ -5349,22 +6762,22 @@ async function defaultShell() {
|
|
|
5349
6762
|
if (pwsh)
|
|
5350
6763
|
return { file: pwsh, args: [], title: "pwsh" };
|
|
5351
6764
|
const system = process.env.SystemRoot ?? "C:\\Windows";
|
|
5352
|
-
const powershell =
|
|
6765
|
+
const powershell = path21.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
|
|
5353
6766
|
if (await exists2(powershell))
|
|
5354
6767
|
return { file: powershell, args: [], title: "powershell" };
|
|
5355
6768
|
return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
|
|
5356
6769
|
}
|
|
5357
6770
|
const shell = process.env.SHELL;
|
|
5358
6771
|
if (shell && await exists2(shell))
|
|
5359
|
-
return { file: shell, args: [], title:
|
|
6772
|
+
return { file: shell, args: [], title: path21.basename(shell) };
|
|
5360
6773
|
if (await exists2("/bin/bash"))
|
|
5361
6774
|
return { file: "/bin/bash", args: [], title: "bash" };
|
|
5362
6775
|
return { file: "/bin/sh", args: [], title: "sh" };
|
|
5363
6776
|
}
|
|
5364
6777
|
async function findOnPath(name) {
|
|
5365
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
6778
|
+
const dirs = (process.env.PATH ?? "").split(path21.delimiter).filter(Boolean);
|
|
5366
6779
|
for (const dir of dirs) {
|
|
5367
|
-
const candidate =
|
|
6780
|
+
const candidate = path21.join(dir, name);
|
|
5368
6781
|
if (await exists2(candidate))
|
|
5369
6782
|
return candidate;
|
|
5370
6783
|
}
|
|
@@ -5375,10 +6788,10 @@ function exists2(file) {
|
|
|
5375
6788
|
}
|
|
5376
6789
|
|
|
5377
6790
|
// ../host/dist/mcp-config.js
|
|
5378
|
-
import { readFile as
|
|
5379
|
-
import
|
|
6791
|
+
import { readFile as readFile16, writeFile as writeFile14, mkdir as mkdir13 } from "node:fs/promises";
|
|
6792
|
+
import path22 from "node:path";
|
|
5380
6793
|
var MCP_FILE = ".mcp.json";
|
|
5381
|
-
var LOCAL_SETTINGS2 =
|
|
6794
|
+
var LOCAL_SETTINGS2 = path22.join(".claude", "settings.local.json");
|
|
5382
6795
|
var queues = /* @__PURE__ */ new Map();
|
|
5383
6796
|
function serialize(cwd, work) {
|
|
5384
6797
|
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
@@ -5387,7 +6800,7 @@ function serialize(cwd, work) {
|
|
|
5387
6800
|
return next;
|
|
5388
6801
|
}
|
|
5389
6802
|
async function readProjectServers(cwd) {
|
|
5390
|
-
const file = await readJson(
|
|
6803
|
+
const file = await readJson(path22.join(cwd, MCP_FILE));
|
|
5391
6804
|
const servers = file?.mcpServers;
|
|
5392
6805
|
if (!servers || typeof servers !== "object")
|
|
5393
6806
|
return {};
|
|
@@ -5395,23 +6808,23 @@ async function readProjectServers(cwd) {
|
|
|
5395
6808
|
}
|
|
5396
6809
|
function addProjectServer(cwd, name, config) {
|
|
5397
6810
|
return serialize(cwd, async () => {
|
|
5398
|
-
const file = await readJson(
|
|
6811
|
+
const file = await readJson(path22.join(cwd, MCP_FILE)) ?? {};
|
|
5399
6812
|
const servers = file.mcpServers ?? {};
|
|
5400
6813
|
servers[name] = config;
|
|
5401
6814
|
file.mcpServers = servers;
|
|
5402
|
-
await writeJson(
|
|
6815
|
+
await writeJson(path22.join(cwd, MCP_FILE), file);
|
|
5403
6816
|
await approve(cwd, name);
|
|
5404
6817
|
});
|
|
5405
6818
|
}
|
|
5406
6819
|
function removeProjectServer(cwd, name) {
|
|
5407
6820
|
return serialize(cwd, async () => {
|
|
5408
|
-
const file = await readJson(
|
|
6821
|
+
const file = await readJson(path22.join(cwd, MCP_FILE));
|
|
5409
6822
|
const servers = file?.mcpServers;
|
|
5410
6823
|
if (!servers || !(name in servers))
|
|
5411
6824
|
return false;
|
|
5412
6825
|
delete servers[name];
|
|
5413
|
-
await writeJson(
|
|
5414
|
-
const settingsPath =
|
|
6826
|
+
await writeJson(path22.join(cwd, MCP_FILE), file);
|
|
6827
|
+
const settingsPath = path22.join(cwd, LOCAL_SETTINGS2);
|
|
5415
6828
|
const settings = await readJson(settingsPath);
|
|
5416
6829
|
const enabled = settings?.enabledMcpjsonServers;
|
|
5417
6830
|
if (settings && Array.isArray(enabled) && enabled.includes(name)) {
|
|
@@ -5422,7 +6835,7 @@ function removeProjectServer(cwd, name) {
|
|
|
5422
6835
|
});
|
|
5423
6836
|
}
|
|
5424
6837
|
async function approve(cwd, name) {
|
|
5425
|
-
const file =
|
|
6838
|
+
const file = path22.join(cwd, LOCAL_SETTINGS2);
|
|
5426
6839
|
const settings = await readJson(file) ?? {};
|
|
5427
6840
|
const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
5428
6841
|
if (!enabled.includes(name))
|
|
@@ -5461,21 +6874,21 @@ function describeConfig(config) {
|
|
|
5461
6874
|
}
|
|
5462
6875
|
async function readJson(file) {
|
|
5463
6876
|
try {
|
|
5464
|
-
const raw = await
|
|
6877
|
+
const raw = await readFile16(file, "utf8");
|
|
5465
6878
|
const parsed = JSON.parse(raw);
|
|
5466
6879
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
5467
6880
|
} catch (error) {
|
|
5468
6881
|
if (error.code === "ENOENT")
|
|
5469
6882
|
return null;
|
|
5470
6883
|
if (error instanceof SyntaxError) {
|
|
5471
|
-
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${
|
|
6884
|
+
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}`);
|
|
5472
6885
|
}
|
|
5473
6886
|
throw error;
|
|
5474
6887
|
}
|
|
5475
6888
|
}
|
|
5476
6889
|
async function writeJson(file, data) {
|
|
5477
|
-
await
|
|
5478
|
-
await
|
|
6890
|
+
await mkdir13(path22.dirname(file), { recursive: true });
|
|
6891
|
+
await writeFile14(file, `${JSON.stringify(data, null, 2)}
|
|
5479
6892
|
`, "utf8");
|
|
5480
6893
|
}
|
|
5481
6894
|
|
|
@@ -5500,6 +6913,7 @@ var HostServer = class {
|
|
|
5500
6913
|
#settings;
|
|
5501
6914
|
#finder;
|
|
5502
6915
|
#sessions = /* @__PURE__ */ new Map();
|
|
6916
|
+
#antigravity;
|
|
5503
6917
|
/** Боковые сессии («вопрос на полях») по id родителя — не больше одной на разговор. */
|
|
5504
6918
|
#sides = /* @__PURE__ */ new Map();
|
|
5505
6919
|
/**
|
|
@@ -5529,7 +6943,14 @@ var HostServer = class {
|
|
|
5529
6943
|
*/
|
|
5530
6944
|
#draftTouchedAt = /* @__PURE__ */ new Map();
|
|
5531
6945
|
#autoStopTimer = null;
|
|
5532
|
-
#
|
|
6946
|
+
#handoffs;
|
|
6947
|
+
#limitStop;
|
|
6948
|
+
#limitTimer = null;
|
|
6949
|
+
/** Идёт остановка у лимита — второй снимок лимитов её не запускает. */
|
|
6950
|
+
#limitStopping = false;
|
|
6951
|
+
/** Живые id сессий, у которых сейчас пишется ручной хэндофф. */
|
|
6952
|
+
#handoffBusy = /* @__PURE__ */ new Set();
|
|
6953
|
+
#hostId = randomUUID5();
|
|
5533
6954
|
#plugins = new PluginCli();
|
|
5534
6955
|
#auth = new AuthCli();
|
|
5535
6956
|
#relay;
|
|
@@ -5552,6 +6973,9 @@ var HostServer = class {
|
|
|
5552
6973
|
this.#order = new ListOrderStore(options2.dataDir);
|
|
5553
6974
|
this.#folders = new Folders(options2.dataDir);
|
|
5554
6975
|
this.#settings = new AppSettingsStore(options2.dataDir);
|
|
6976
|
+
this.#antigravity = new AntigravityStore(options2.dataDir);
|
|
6977
|
+
this.#handoffs = new HandoffStore(options2.dataDir);
|
|
6978
|
+
this.#limitStop = new LimitStopState(options2.dataDir);
|
|
5555
6979
|
this.#finder = new ProjectFinder(this.#workspaces);
|
|
5556
6980
|
this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
|
|
5557
6981
|
this.#github = new GitHubAccount(options2.dataDir);
|
|
@@ -5571,6 +6995,7 @@ var HostServer = class {
|
|
|
5571
6995
|
await this.#order.load();
|
|
5572
6996
|
await this.#folders.load();
|
|
5573
6997
|
await this.#settings.load();
|
|
6998
|
+
await this.#limitStop.load();
|
|
5574
6999
|
await this.#github.load();
|
|
5575
7000
|
await warmClaudeLauncher();
|
|
5576
7001
|
void this.#finder.warm();
|
|
@@ -5595,11 +7020,16 @@ var HostServer = class {
|
|
|
5595
7020
|
});
|
|
5596
7021
|
this.#autoStopTimer = setInterval(() => void this.#stopIdleSessions(), AUTO_STOP_SWEEP_MS);
|
|
5597
7022
|
this.#autoStopTimer.unref();
|
|
7023
|
+
this.#limitTimer = setInterval(() => this.#sweepUsage(), LIMIT_SWEEP_MS);
|
|
7024
|
+
this.#limitTimer.unref();
|
|
5598
7025
|
}
|
|
5599
7026
|
async shutdown() {
|
|
5600
7027
|
if (this.#autoStopTimer)
|
|
5601
7028
|
clearInterval(this.#autoStopTimer);
|
|
5602
7029
|
this.#autoStopTimer = null;
|
|
7030
|
+
if (this.#limitTimer)
|
|
7031
|
+
clearInterval(this.#limitTimer);
|
|
7032
|
+
this.#limitTimer = null;
|
|
5603
7033
|
this.#auth.cancel();
|
|
5604
7034
|
this.#terminals.closeAll();
|
|
5605
7035
|
await this.#relay.close();
|
|
@@ -6338,6 +7768,24 @@ var HostServer = class {
|
|
|
6338
7768
|
case "settings.get":
|
|
6339
7769
|
this.#send(socket, { type: "settings.state", settings: this.#settings.current });
|
|
6340
7770
|
return;
|
|
7771
|
+
case "handoff.get":
|
|
7772
|
+
this.#send(socket, {
|
|
7773
|
+
type: "handoff.state",
|
|
7774
|
+
sessionId: frame.sessionId,
|
|
7775
|
+
handoff: await this.#handoffs.get(frame.sessionId)
|
|
7776
|
+
});
|
|
7777
|
+
return;
|
|
7778
|
+
case "handoff.dismiss":
|
|
7779
|
+
await this.#handoffs.remove(frame.sessionId);
|
|
7780
|
+
this.#broadcast({ type: "handoff.state", sessionId: frame.sessionId, handoff: null });
|
|
7781
|
+
return;
|
|
7782
|
+
case "session.handoff":
|
|
7783
|
+
this.#send(socket, {
|
|
7784
|
+
type: "handoff.written",
|
|
7785
|
+
sessionId: frame.sessionId,
|
|
7786
|
+
handoff: await this.#manualHandoff(frame.sessionId)
|
|
7787
|
+
});
|
|
7788
|
+
return;
|
|
6341
7789
|
case "settings.update": {
|
|
6342
7790
|
const before = this.#settings.current;
|
|
6343
7791
|
const settings = await this.#settings.update(frame.settings);
|
|
@@ -6600,6 +8048,124 @@ var HostServer = class {
|
|
|
6600
8048
|
this.#draftTouchedAt.delete(session.id);
|
|
6601
8049
|
}
|
|
6602
8050
|
}
|
|
8051
|
+
// -------------------------------------------------------------------------
|
|
8052
|
+
// Остановка у лимита плана
|
|
8053
|
+
// -------------------------------------------------------------------------
|
|
8054
|
+
/** Пользовательские сессии Claude — те, что останавливаются у лимита. */
|
|
8055
|
+
#claudeUserSessions() {
|
|
8056
|
+
return [...this.#sessions.values()].filter((s) => s instanceof ClaudeSession && s !== this.#finderSession && Boolean(s.workspaceId) && !s.summary().parentSessionId);
|
|
8057
|
+
}
|
|
8058
|
+
#isBusy(session) {
|
|
8059
|
+
const summary = session.summary();
|
|
8060
|
+
return BUSY_STATES.has(summary.state) || summary.backgroundWork;
|
|
8061
|
+
}
|
|
8062
|
+
/**
|
|
8063
|
+
* Перечитать лимиты, пока хоть одна сессия занята. Сама сессия делает это
|
|
8064
|
+
* по результатам инструментов, а долгий шаг модели без инструментов иначе
|
|
8065
|
+
* прошёл бы мимо порога. Лимиты общие на аккаунт — спросить одну хватит.
|
|
8066
|
+
*/
|
|
8067
|
+
#sweepUsage() {
|
|
8068
|
+
const busy = this.#claudeUserSessions().find((s) => this.#isBusy(s));
|
|
8069
|
+
if (busy)
|
|
8070
|
+
void busy.refreshUsage();
|
|
8071
|
+
}
|
|
8072
|
+
async #checkLimit(usage) {
|
|
8073
|
+
if (this.#limitStopping)
|
|
8074
|
+
return;
|
|
8075
|
+
const hit = crossedLimit(usage, this.#settings.current, (limit) => this.#limitStop.fired(limit));
|
|
8076
|
+
if (!hit)
|
|
8077
|
+
return;
|
|
8078
|
+
this.#limitStopping = true;
|
|
8079
|
+
try {
|
|
8080
|
+
await this.#limitStop.mark(hit.limit, hit.until);
|
|
8081
|
+
await this.#stopAtLimit(hit);
|
|
8082
|
+
} catch (error) {
|
|
8083
|
+
console.error("[limit-stop]", error instanceof Error ? error.message : String(error));
|
|
8084
|
+
} finally {
|
|
8085
|
+
this.#limitStopping = false;
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
/**
|
|
8089
|
+
* Прервать работу у лимита: занятые сессии пишут хэндофф (или он
|
|
8090
|
+
* собирается из журнала, если агент не ответил), простаивающие не
|
|
8091
|
+
* трогаются. Процессы сессий остаются жить — гаснет сессия только тогда,
|
|
8092
|
+
* когда из её хэндоффа продолжили в новой (см. `continueFromHandoff` в
|
|
8093
|
+
* окне).
|
|
8094
|
+
*/
|
|
8095
|
+
async #stopAtLimit(hit) {
|
|
8096
|
+
const sessions = this.#claudeUserSessions();
|
|
8097
|
+
if (sessions.length === 0)
|
|
8098
|
+
return;
|
|
8099
|
+
const busy = sessions.filter((s) => this.#isBusy(s));
|
|
8100
|
+
const prompt = handoffPrompt(hit);
|
|
8101
|
+
const results = await Promise.all(busy.map(async (session) => {
|
|
8102
|
+
const text = await session.runHandoff(prompt, HANDOFF_TIMEOUT_MS);
|
|
8103
|
+
const summary = session.summary();
|
|
8104
|
+
const handoff = {
|
|
8105
|
+
sessionId: summary.claudeSessionId ?? session.id,
|
|
8106
|
+
workspaceId: session.workspaceId,
|
|
8107
|
+
title: summary.title ?? "",
|
|
8108
|
+
limit: hit.limit,
|
|
8109
|
+
utilization: hit.window.utilization,
|
|
8110
|
+
resetsAt: hit.window.resetsAt,
|
|
8111
|
+
stoppedAt: Date.now(),
|
|
8112
|
+
source: text ? "agent" : "journal",
|
|
8113
|
+
text: text ?? journalHandoff(session.journal.since(0))
|
|
8114
|
+
};
|
|
8115
|
+
try {
|
|
8116
|
+
await this.#handoffs.save(handoff);
|
|
8117
|
+
this.#broadcast({ type: "handoff.state", sessionId: handoff.sessionId, handoff });
|
|
8118
|
+
return true;
|
|
8119
|
+
} catch (error) {
|
|
8120
|
+
console.error("[limit-stop]", error instanceof Error ? error.message : String(error));
|
|
8121
|
+
return false;
|
|
8122
|
+
}
|
|
8123
|
+
}));
|
|
8124
|
+
this.#broadcast({
|
|
8125
|
+
type: "limit.stopped",
|
|
8126
|
+
limit: hit.limit,
|
|
8127
|
+
utilization: hit.window.utilization,
|
|
8128
|
+
resetsAt: hit.window.resetsAt,
|
|
8129
|
+
stopped: results.length,
|
|
8130
|
+
handoffs: results.filter(Boolean).length
|
|
8131
|
+
});
|
|
8132
|
+
}
|
|
8133
|
+
/**
|
|
8134
|
+
* «Хэндофф и новая сессия»: тот же ход хэндоффа, что у лимита, только по
|
|
8135
|
+
* просьбе пользователя. Сессия после него останавливается — работа
|
|
8136
|
+
* переезжает в новую, которую заведёт спросившее окно (см.
|
|
8137
|
+
* `handoff.written`). `null` — писать не из чего: сессии нет, она не
|
|
8138
|
+
* Claude или уже пишет хэндофф (например, второе нажатие).
|
|
8139
|
+
*/
|
|
8140
|
+
async #manualHandoff(sessionId) {
|
|
8141
|
+
const session = this.#sessions.get(sessionId);
|
|
8142
|
+
if (!(session instanceof ClaudeSession) || session.summary().state === "closed")
|
|
8143
|
+
return null;
|
|
8144
|
+
if (this.#handoffBusy.has(sessionId))
|
|
8145
|
+
return null;
|
|
8146
|
+
this.#handoffBusy.add(sessionId);
|
|
8147
|
+
try {
|
|
8148
|
+
const text = await session.runHandoff(handoffPrompt(null), HANDOFF_TIMEOUT_MS);
|
|
8149
|
+
const summary = session.summary();
|
|
8150
|
+
const handoff = {
|
|
8151
|
+
sessionId: summary.claudeSessionId ?? session.id,
|
|
8152
|
+
workspaceId: session.workspaceId,
|
|
8153
|
+
title: summary.title ?? "",
|
|
8154
|
+
limit: null,
|
|
8155
|
+
utilization: 0,
|
|
8156
|
+
resetsAt: "",
|
|
8157
|
+
stoppedAt: Date.now(),
|
|
8158
|
+
source: text ? "agent" : "journal",
|
|
8159
|
+
text: text ?? journalHandoff(session.journal.since(0))
|
|
8160
|
+
};
|
|
8161
|
+
await this.#handoffs.save(handoff);
|
|
8162
|
+
this.#broadcast({ type: "handoff.state", sessionId: handoff.sessionId, handoff });
|
|
8163
|
+
await this.#dropSession(session.id, "handoff").catch(() => false);
|
|
8164
|
+
return handoff;
|
|
8165
|
+
} finally {
|
|
8166
|
+
this.#handoffBusy.delete(sessionId);
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
6603
8169
|
/**
|
|
6604
8170
|
* Живые сессии всех проектов — вкладке запущенных.
|
|
6605
8171
|
*
|
|
@@ -6654,6 +8220,26 @@ var HostServer = class {
|
|
|
6654
8220
|
const workspace = this.#workspaces.find(frame.workspaceId);
|
|
6655
8221
|
if (!workspace)
|
|
6656
8222
|
throw new Error(`\u0412\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${frame.workspaceId}`);
|
|
8223
|
+
const agyRecord = frame.resume ? await this.#antigravity.find(frame.resume) : null;
|
|
8224
|
+
if (agyRecord || !frame.resume && frame.provider === "antigravity") {
|
|
8225
|
+
if (agyRecord)
|
|
8226
|
+
await this.#archive.unarchive([agyRecord.id]);
|
|
8227
|
+
const session2 = new AntigravitySession({
|
|
8228
|
+
workspaceId: workspace.id,
|
|
8229
|
+
cwd: agyRecord?.cwd ?? workspace.path,
|
|
8230
|
+
store: this.#antigravity,
|
|
8231
|
+
...agyRecord ? { record: agyRecord } : {},
|
|
8232
|
+
...frame.permissionMode ? { permissionMode: frame.permissionMode } : {},
|
|
8233
|
+
...frame.model ? { model: frame.model } : {},
|
|
8234
|
+
defaultTitle: workspace.name
|
|
8235
|
+
});
|
|
8236
|
+
this.#wire(session2);
|
|
8237
|
+
this.#sessions.set(session2.id, session2);
|
|
8238
|
+
session2.start();
|
|
8239
|
+
this.#sendLive();
|
|
8240
|
+
this.#send(socket, { type: "session.summary", session: session2.summary() });
|
|
8241
|
+
return;
|
|
8242
|
+
}
|
|
6657
8243
|
const resumed = frame.resume && !frame.fork ? (await this.#workspaces.sessionsOnDisk(workspace.id)).find((info) => info.sessionId === frame.resume) : void 0;
|
|
6658
8244
|
const resumedTitle = resumed ? diskTitle(resumed) : null;
|
|
6659
8245
|
if (frame.resume)
|
|
@@ -6677,6 +8263,44 @@ var HostServer = class {
|
|
|
6677
8263
|
session.start();
|
|
6678
8264
|
this.#sendLive();
|
|
6679
8265
|
this.#send(socket, { type: "session.summary", session: session.summary() });
|
|
8266
|
+
if (frame.continuesFrom)
|
|
8267
|
+
await this.#carryPin(workspace.id, frame.continuesFrom, session.id);
|
|
8268
|
+
}
|
|
8269
|
+
/**
|
|
8270
|
+
* «Продолжить в новой сессии» из хэндоффа: закрепление прежней сессии
|
|
8271
|
+
* переезжает на новую — вместе с местом в панели и папкой закреплённых.
|
|
8272
|
+
* Работа продолжается уже там, и мёртвая сессия у лимита не должна
|
|
8273
|
+
* занимать её строку, а новая — теряться в общем списке.
|
|
8274
|
+
*
|
|
8275
|
+
* Новая закрепляется под своим локальным uuid: дискового id у неё ещё нет,
|
|
8276
|
+
* на него запись переедет сама, как у любой свежей закреплённой (см.
|
|
8277
|
+
* `#wire`). У прежней снимаем оба id — она может быть ещё жива, и тогда
|
|
8278
|
+
* закрепление записано и под её uuid (см. `case 'session.pin'`).
|
|
8279
|
+
*/
|
|
8280
|
+
async #carryPin(workspaceId, fromId, toId) {
|
|
8281
|
+
const fromIds = [fromId];
|
|
8282
|
+
for (const live of this.#sessions.values()) {
|
|
8283
|
+
if (live.id !== toId && live.claudeSessionId === fromId)
|
|
8284
|
+
fromIds.push(live.id);
|
|
8285
|
+
}
|
|
8286
|
+
if (!await this.#pins.transfer(fromIds, toId))
|
|
8287
|
+
return;
|
|
8288
|
+
for (const id of fromIds)
|
|
8289
|
+
await this.#folders.migratePinnedSession(id, toId);
|
|
8290
|
+
await this.#sendSessionList(workspaceId);
|
|
8291
|
+
}
|
|
8292
|
+
/**
|
|
8293
|
+
* Переезд закрепления на новый id той же сессии — вместе с папкой
|
|
8294
|
+
* закреплённых, иначе строка выпала бы из своей папки в общий список.
|
|
8295
|
+
*/
|
|
8296
|
+
async #migratePin(fromId, toId) {
|
|
8297
|
+
try {
|
|
8298
|
+
const moved = await this.#pins.migrate(fromId, toId);
|
|
8299
|
+
await this.#folders.migratePinnedSession(fromId, toId);
|
|
8300
|
+
if (moved)
|
|
8301
|
+
await this.#sendPinned();
|
|
8302
|
+
} catch {
|
|
8303
|
+
}
|
|
6680
8304
|
}
|
|
6681
8305
|
/**
|
|
6682
8306
|
* Подписка на события сессии.
|
|
@@ -6692,12 +8316,12 @@ var HostServer = class {
|
|
|
6692
8316
|
this.#broadcast({ type: "session.summary", session: summary });
|
|
6693
8317
|
if (!summary.claudeSessionId || !this.#pins.has(summary.id))
|
|
6694
8318
|
return;
|
|
6695
|
-
void this.#
|
|
8319
|
+
void this.#migratePin(summary.id, summary.claudeSessionId);
|
|
6696
8320
|
});
|
|
6697
8321
|
session.on("claudeSessionId", ({ from, to }) => {
|
|
6698
8322
|
if (!this.#pins.has(from))
|
|
6699
8323
|
return;
|
|
6700
|
-
void this.#
|
|
8324
|
+
void this.#migratePin(from, to);
|
|
6701
8325
|
});
|
|
6702
8326
|
session.on("capabilities", () => {
|
|
6703
8327
|
if (!session.capabilities)
|
|
@@ -6710,12 +8334,15 @@ var HostServer = class {
|
|
|
6710
8334
|
});
|
|
6711
8335
|
session.on("usage", (usage) => {
|
|
6712
8336
|
this.#broadcast({ type: "usage.state", sessionId: session.id, usage });
|
|
8337
|
+
if (session instanceof ClaudeSession)
|
|
8338
|
+
void this.#checkLimit(usage);
|
|
6713
8339
|
});
|
|
6714
8340
|
session.on("context", (context) => {
|
|
6715
8341
|
this.#broadcast({ type: "context.state", sessionId: session.id, context });
|
|
6716
8342
|
});
|
|
6717
8343
|
session.on("stderr", (data) => {
|
|
6718
|
-
|
|
8344
|
+
const tag = session instanceof AntigravitySession ? "agy" : "claude";
|
|
8345
|
+
process.stderr.write(`[${tag} ${session.id.slice(0, 8)}] ${data}`);
|
|
6719
8346
|
});
|
|
6720
8347
|
}
|
|
6721
8348
|
// -------------------------------------------------------------------------
|
|
@@ -6734,6 +8361,9 @@ var HostServer = class {
|
|
|
6734
8361
|
* разговора пока и не было.
|
|
6735
8362
|
*/
|
|
6736
8363
|
#ensureSide(parent) {
|
|
8364
|
+
if (parent instanceof AntigravitySession) {
|
|
8365
|
+
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");
|
|
8366
|
+
}
|
|
6737
8367
|
const existing = this.#sides.get(parent.id);
|
|
6738
8368
|
if (existing && existing.state !== "closed" && existing.state !== "error") {
|
|
6739
8369
|
return existing;
|
|
@@ -7031,6 +8661,11 @@ var HostServer = class {
|
|
|
7031
8661
|
live.setTitle(title);
|
|
7032
8662
|
return;
|
|
7033
8663
|
}
|
|
8664
|
+
if (await this.#antigravity.rename(sessionId, title)) {
|
|
8665
|
+
if (workspaceId)
|
|
8666
|
+
await this.#sendSessionList(workspaceId);
|
|
8667
|
+
return;
|
|
8668
|
+
}
|
|
7034
8669
|
const dir = workspaceId ? this.#workspaces.find(workspaceId)?.path : void 0;
|
|
7035
8670
|
await renameSession2(sessionId, title, dir ? { dir } : void 0);
|
|
7036
8671
|
if (!workspaceId)
|
|
@@ -7052,6 +8687,7 @@ var HostServer = class {
|
|
|
7052
8687
|
const workspacePath = this.#workspaces.find(workspaceId)?.path ?? "";
|
|
7053
8688
|
const historical = onDisk.filter((info) => !liveIds.has(info.sessionId) && info.customTitle !== SIDE_TITLE && info.customTitle !== FINDER_TITLE).map((info) => ({
|
|
7054
8689
|
id: info.sessionId,
|
|
8690
|
+
provider: "claude",
|
|
7055
8691
|
claudeSessionId: info.sessionId,
|
|
7056
8692
|
workspaceId,
|
|
7057
8693
|
cwd: info.cwd ?? workspacePath,
|
|
@@ -7076,6 +8712,11 @@ var HostServer = class {
|
|
|
7076
8712
|
lastSeq: 0,
|
|
7077
8713
|
totalCostUsd: 0
|
|
7078
8714
|
}));
|
|
8715
|
+
for (const record of await this.#antigravity.list(workspaceId)) {
|
|
8716
|
+
if (liveIds.has(record.id))
|
|
8717
|
+
continue;
|
|
8718
|
+
historical.push(antigravitySummary(record));
|
|
8719
|
+
}
|
|
7079
8720
|
const known = /* @__PURE__ */ new Set([...liveIds, ...historical.map((s) => s.id)]);
|
|
7080
8721
|
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);
|
|
7081
8722
|
await this.#pins.sweep(workspaceId, known);
|
|
@@ -7128,8 +8769,8 @@ var HostServer = class {
|
|
|
7128
8769
|
return;
|
|
7129
8770
|
}
|
|
7130
8771
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
7131
|
-
const requested =
|
|
7132
|
-
const target = requested.startsWith(webRoot) && existsSync4(requested) && statSync3(requested).isFile() ? requested :
|
|
8772
|
+
const requested = path23.join(webRoot, path23.normalize(url2.pathname));
|
|
8773
|
+
const target = requested.startsWith(webRoot) && existsSync4(requested) && statSync3(requested).isFile() ? requested : path23.join(webRoot, "index.html");
|
|
7133
8774
|
if (!existsSync4(target)) {
|
|
7134
8775
|
res.writeHead(404).end("not found");
|
|
7135
8776
|
return;
|
|
@@ -7155,12 +8796,36 @@ function contentType(file) {
|
|
|
7155
8796
|
".svg": "image/svg+xml",
|
|
7156
8797
|
".woff2": "font/woff2"
|
|
7157
8798
|
};
|
|
7158
|
-
return types[
|
|
8799
|
+
return types[path23.extname(file)] ?? "application/octet-stream";
|
|
8800
|
+
}
|
|
8801
|
+
function antigravitySummary(record) {
|
|
8802
|
+
return {
|
|
8803
|
+
id: record.id,
|
|
8804
|
+
provider: "antigravity",
|
|
8805
|
+
claudeSessionId: record.id,
|
|
8806
|
+
workspaceId: record.workspaceId,
|
|
8807
|
+
cwd: record.cwd,
|
|
8808
|
+
parentSessionId: null,
|
|
8809
|
+
title: record.title,
|
|
8810
|
+
state: "closed",
|
|
8811
|
+
lastTurn: null,
|
|
8812
|
+
backgroundWork: false,
|
|
8813
|
+
model: record.model,
|
|
8814
|
+
permissionMode: record.permissionMode,
|
|
8815
|
+
effort: record.effort,
|
|
8816
|
+
ultracode: false,
|
|
8817
|
+
thinking: true,
|
|
8818
|
+
switchModelsOnFlag: true,
|
|
8819
|
+
createdAt: record.createdAt,
|
|
8820
|
+
updatedAt: record.updatedAt,
|
|
8821
|
+
lastSeq: 0,
|
|
8822
|
+
totalCostUsd: 0
|
|
8823
|
+
};
|
|
7159
8824
|
}
|
|
7160
8825
|
|
|
7161
8826
|
// dist/ccv.js
|
|
7162
|
-
var HERE =
|
|
7163
|
-
var PACKAGE_ROOT =
|
|
8827
|
+
var HERE = path24.dirname(fileURLToPath(import.meta.url));
|
|
8828
|
+
var PACKAGE_ROOT = path24.resolve(HERE, "..");
|
|
7164
8829
|
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.
|
|
7165
8830
|
|
|
7166
8831
|
\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
|
|
@@ -7227,7 +8892,7 @@ var server = new HostServer({
|
|
|
7227
8892
|
dataDir: defaults.dataDir,
|
|
7228
8893
|
// UI лежит в пакете рядом с бандлом — это и есть разница между установленной
|
|
7229
8894
|
// утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
|
|
7230
|
-
webRoot: process.env.CCV_WEB_ROOT ??
|
|
8895
|
+
webRoot: process.env.CCV_WEB_ROOT ?? path24.join(PACKAGE_ROOT, "web"),
|
|
7231
8896
|
version,
|
|
7232
8897
|
relayUrl: defaults.relayUrl
|
|
7233
8898
|
});
|
|
@@ -7256,7 +8921,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
7256
8921
|
}
|
|
7257
8922
|
async function packageVersion() {
|
|
7258
8923
|
try {
|
|
7259
|
-
const text = await
|
|
8924
|
+
const text = await readFile17(path24.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
7260
8925
|
return JSON.parse(text).version ?? "0.0.0";
|
|
7261
8926
|
} catch {
|
|
7262
8927
|
return "0.0.0";
|
|
@@ -7300,7 +8965,7 @@ function openBrowser(target) {
|
|
|
7300
8965
|
return;
|
|
7301
8966
|
}
|
|
7302
8967
|
const command = process.platform === "win32" ? { file: "cmd", args: ["/c", "start", "", target] } : process.platform === "darwin" ? { file: "open", args: [target] } : { file: "xdg-open", args: [target] };
|
|
7303
|
-
const child =
|
|
8968
|
+
const child = spawn3(command.file, command.args, {
|
|
7304
8969
|
stdio: "ignore",
|
|
7305
8970
|
detached: true,
|
|
7306
8971
|
windowsHide: true
|