@i14ks/ccv 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +91 -5
- package/bundle/ccv.mjs +1008 -60
- package/package.json +2 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/_commonjsHelpers-CqkleIqs.js +1 -0
- package/web/assets/addon-fit-CevVMrUA.js +1 -0
- package/web/assets/addon-web-links-CnB65OV_.js +1 -0
- package/web/assets/codemirror-nMcHCEIz.js +1 -0
- package/web/assets/index-0OKbY2Rb.js +7 -0
- package/web/assets/index-3-HwFAxb.js +3 -0
- package/web/assets/index-891wE-Rj.js +7 -0
- package/web/assets/index-B3rRxMhp.js +1 -0
- package/web/assets/index-BGGB4Isr.js +1 -0
- package/web/assets/index-BHot-lnj.js +5 -0
- package/web/assets/index-BJ3X5lFg.js +1 -0
- package/web/assets/index-BQl70IYm.js +1 -0
- package/web/assets/{index-C9vrRfhW.js → index-Bh6TpXPz.js} +3 -3
- package/web/assets/index-CHho5YQ1.js +1 -0
- package/web/assets/index-CJsOv-xc.js +128 -0
- package/web/assets/index-CQJq2xwX.css +1 -0
- package/web/assets/index-Cb6NO7uV.js +1 -0
- package/web/assets/{index-1iYnLw7q.js → index-Cg1cP-qO.js} +1 -1
- package/web/assets/index-Cs6uqj5d.js +1 -0
- package/web/assets/index-D1KJL4bg.js +1 -0
- package/web/assets/index-DOgeK81c.js +1 -0
- package/web/assets/index-Dj0LEd8w.js +13 -0
- package/web/assets/index-b5KaknlY.js +1 -0
- package/web/assets/index-cpz7Q_hZ.js +1 -0
- package/web/assets/index-lrIwyqs2.js +1 -0
- package/web/assets/{index-D-buiUaW.js → index-wD2t70jv.js} +2 -2
- package/web/assets/index-wu5Pm3nc.js +2 -0
- package/web/assets/xterm-D6Mng-DB.js +9 -0
- package/web/assets/xterm-DYP7pi_n.css +32 -0
- package/web/favicon.ico +0 -0
- package/web/favicon.png +0 -0
- package/web/favicon.svg +4 -0
- package/web/index.html +12 -2
- package/web/assets/index-7poo-d50.js +0 -1
- package/web/assets/index-BMMMWgeR.js +0 -13
- package/web/assets/index-BW8WtNtA.js +0 -1
- package/web/assets/index-C7CZdm1C.js +0 -3
- package/web/assets/index-CVP8I8tU.js +0 -1
- package/web/assets/index-CpwLxLdo.js +0 -1
- package/web/assets/index-DCtzpGvd.js +0 -133
- package/web/assets/index-DDnDtQl5.js +0 -1
- package/web/assets/index-DKwcT9vY.js +0 -1
- package/web/assets/index-DYr0npzc.css +0 -1
- package/web/assets/index-DcGWR_5B.js +0 -1
- package/web/assets/index-PCsjZn72.js +0 -1
- package/web/assets/index-XBmPtinY.js +0 -7
- package/web/assets/index-gDk09Gbz.js +0 -1
package/bundle/ccv.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// dist/ccv.js
|
|
4
4
|
import { spawn as spawn2 } from "node:child_process";
|
|
5
|
-
import { readFile as
|
|
6
|
-
import
|
|
5
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
6
|
+
import path16 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 existsSync3, statSync as statSync3 } from "node:fs";
|
|
149
|
-
import
|
|
149
|
+
import path15 from "node:path";
|
|
150
150
|
import { homedir as homedir5 } from "node:os";
|
|
151
|
-
import { randomUUID as
|
|
151
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
152
152
|
import { WebSocketServer } from "ws";
|
|
153
153
|
import { renameSession as renameSession2 } from "@anthropic-ai/claude-agent-sdk";
|
|
154
154
|
|
|
@@ -628,6 +628,184 @@ async function readSettings(file) {
|
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
+
// ../host/dist/transcript.js
|
|
632
|
+
import { getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
|
|
633
|
+
var RESULT_LIMIT = 4e3;
|
|
634
|
+
var INPUT_LIMIT = 2e4;
|
|
635
|
+
var CUT = "\n\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)";
|
|
636
|
+
async function readTranscript(sessionId, options2) {
|
|
637
|
+
let messages;
|
|
638
|
+
try {
|
|
639
|
+
messages = await getSessionMessages(sessionId, {
|
|
640
|
+
dir: options2.dir,
|
|
641
|
+
includeSystemMessages: true
|
|
642
|
+
});
|
|
643
|
+
} catch {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
const entries = [];
|
|
647
|
+
const blockCounts = /* @__PURE__ */ new Map();
|
|
648
|
+
let seq = 0;
|
|
649
|
+
const record = (event, ts) => {
|
|
650
|
+
entries.push({ seq: ++seq, ts, event });
|
|
651
|
+
};
|
|
652
|
+
for (const message of messages) {
|
|
653
|
+
if (message.parent_tool_use_id !== null || message.parent_agent_id !== null)
|
|
654
|
+
continue;
|
|
655
|
+
const ts = timestampOf(message);
|
|
656
|
+
if (ts >= options2.until)
|
|
657
|
+
continue;
|
|
658
|
+
if (message.type === "system") {
|
|
659
|
+
if (subtypeOf(message) === "compact_boundary") {
|
|
660
|
+
record({ kind: "system", subtype: "compact_boundary", data: {} }, ts);
|
|
661
|
+
}
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const content = contentOf(message);
|
|
665
|
+
if (message.type === "user") {
|
|
666
|
+
if (typeof content === "string") {
|
|
667
|
+
const text = humanText(content);
|
|
668
|
+
if (text)
|
|
669
|
+
record({ kind: "user.message", text }, ts);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
for (const block of content) {
|
|
673
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
674
|
+
const text = humanText(block.text);
|
|
675
|
+
if (text)
|
|
676
|
+
record({ kind: "user.message", text }, ts);
|
|
677
|
+
} else if (block.type === "tool_result") {
|
|
678
|
+
record({
|
|
679
|
+
kind: "tool.result",
|
|
680
|
+
toolUseId: String(block.tool_use_id ?? ""),
|
|
681
|
+
content: clip(contentToText(block.content), RESULT_LIMIT),
|
|
682
|
+
isError: block.is_error === true
|
|
683
|
+
}, ts);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const messageId = messageIdOf(message);
|
|
689
|
+
if (typeof content === "string") {
|
|
690
|
+
if (content)
|
|
691
|
+
record({ kind: "assistant.text", text: content, blockId: `${messageId}:0` }, ts);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
for (const block of content) {
|
|
695
|
+
const blockId = `${messageId}:${nextBlockIndex(blockCounts, messageId)}`;
|
|
696
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
697
|
+
if (!block.text)
|
|
698
|
+
continue;
|
|
699
|
+
if (synthetic(message)) {
|
|
700
|
+
record({ kind: "error", message: block.text }, ts);
|
|
701
|
+
} else {
|
|
702
|
+
record({ kind: "assistant.text", text: block.text, blockId }, ts);
|
|
703
|
+
}
|
|
704
|
+
} else if (block.type === "thinking") {
|
|
705
|
+
const text = typeof block.thinking === "string" ? block.thinking : "";
|
|
706
|
+
if (text)
|
|
707
|
+
record({ kind: "assistant.thinking", text, blockId }, ts);
|
|
708
|
+
} else if (block.type === "tool_use") {
|
|
709
|
+
record({
|
|
710
|
+
kind: "tool.use",
|
|
711
|
+
toolUseId: String(block.id ?? ""),
|
|
712
|
+
name: String(block.name ?? ""),
|
|
713
|
+
input: clipInput(block.input)
|
|
714
|
+
}, ts);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return entries;
|
|
719
|
+
}
|
|
720
|
+
function asObject(value) {
|
|
721
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
722
|
+
}
|
|
723
|
+
function contentOf(message) {
|
|
724
|
+
const content = asObject(message.message)?.content;
|
|
725
|
+
if (typeof content === "string")
|
|
726
|
+
return content;
|
|
727
|
+
if (!Array.isArray(content))
|
|
728
|
+
return [];
|
|
729
|
+
return content.filter((block) => typeof block === "object" && block !== null);
|
|
730
|
+
}
|
|
731
|
+
function messageIdOf(message) {
|
|
732
|
+
const id = asObject(message.message)?.id;
|
|
733
|
+
return typeof id === "string" ? id : message.uuid;
|
|
734
|
+
}
|
|
735
|
+
function synthetic(message) {
|
|
736
|
+
return asObject(message.message)?.model === "<synthetic>";
|
|
737
|
+
}
|
|
738
|
+
function subtypeOf(message) {
|
|
739
|
+
const subtype = message.subtype;
|
|
740
|
+
return typeof subtype === "string" ? subtype : "";
|
|
741
|
+
}
|
|
742
|
+
function timestampOf(message) {
|
|
743
|
+
const raw = message.timestamp;
|
|
744
|
+
if (typeof raw !== "string")
|
|
745
|
+
return 0;
|
|
746
|
+
const ts = Date.parse(raw);
|
|
747
|
+
return Number.isNaN(ts) ? 0 : ts;
|
|
748
|
+
}
|
|
749
|
+
function nextBlockIndex(counts, messageId) {
|
|
750
|
+
const index = counts.get(messageId) ?? 0;
|
|
751
|
+
counts.set(messageId, index + 1);
|
|
752
|
+
return index;
|
|
753
|
+
}
|
|
754
|
+
var ENVELOPE = /^\s*<(local-command-caveat|local-command-stdout|system-reminder|task-notification|command-message|command-args)\b/;
|
|
755
|
+
var SLASH_COMMAND = /^\s*<command-name>([^<]*)<\/command-name>/;
|
|
756
|
+
var SLASH_ARGS = /<command-args>([^<]*)<\/command-args>/;
|
|
757
|
+
function humanText(raw) {
|
|
758
|
+
const slash = SLASH_COMMAND.exec(raw);
|
|
759
|
+
if (slash) {
|
|
760
|
+
const name = (slash[1] ?? "").trim();
|
|
761
|
+
const args = (SLASH_ARGS.exec(raw)?.[1] ?? "").trim();
|
|
762
|
+
return args ? `${name} ${args}` : name;
|
|
763
|
+
}
|
|
764
|
+
if (ENVELOPE.test(raw))
|
|
765
|
+
return "";
|
|
766
|
+
const text = raw.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
|
767
|
+
if (/^\[Image: /.test(text))
|
|
768
|
+
return "";
|
|
769
|
+
return text;
|
|
770
|
+
}
|
|
771
|
+
function contentToText(content) {
|
|
772
|
+
if (typeof content === "string")
|
|
773
|
+
return content;
|
|
774
|
+
if (!Array.isArray(content))
|
|
775
|
+
return "";
|
|
776
|
+
return content.map((block) => {
|
|
777
|
+
if (typeof block === "string")
|
|
778
|
+
return block;
|
|
779
|
+
const item = asObject(block);
|
|
780
|
+
if (!item)
|
|
781
|
+
return "";
|
|
782
|
+
if (typeof item.text === "string")
|
|
783
|
+
return item.text;
|
|
784
|
+
if (typeof item.type === "string")
|
|
785
|
+
return `[${item.type}]`;
|
|
786
|
+
return "";
|
|
787
|
+
}).join("");
|
|
788
|
+
}
|
|
789
|
+
function clip(text, limit) {
|
|
790
|
+
return text.length > limit ? text.slice(0, limit) + CUT : text;
|
|
791
|
+
}
|
|
792
|
+
function clipInput(input) {
|
|
793
|
+
const object = asObject(input);
|
|
794
|
+
if (!object)
|
|
795
|
+
return input;
|
|
796
|
+
let changed = false;
|
|
797
|
+
const clipped = {};
|
|
798
|
+
for (const [key, value] of Object.entries(object)) {
|
|
799
|
+
if (typeof value === "string" && value.length > INPUT_LIMIT) {
|
|
800
|
+
clipped[key] = clip(value, INPUT_LIMIT);
|
|
801
|
+
changed = true;
|
|
802
|
+
} else {
|
|
803
|
+
clipped[key] = value;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return changed ? clipped : input;
|
|
807
|
+
}
|
|
808
|
+
|
|
631
809
|
// ../host/dist/session.js
|
|
632
810
|
function uiSystemPromptAppend(cwd) {
|
|
633
811
|
const markdown = "You are running inside Claude Code Visual, a GUI for Claude Code. When the user opens a .md file in this app, it is rendered as formatted, human-readable text (headings, lists, code blocks, tables) by default \u2014 not shown as raw markdown source. A toggle lets them switch to the raw source view if they want to edit it. So when producing a report, plan, summary, or any other document meant for the user to read, prefer writing it to a .md file rather than only in chat \u2014 the user will see it rendered, not as plain marked-up text.";
|
|
@@ -647,6 +825,29 @@ function writeTarget(input) {
|
|
|
647
825
|
return typeof target === "string" && target ? target : null;
|
|
648
826
|
}
|
|
649
827
|
var ASK_QUESTION_TOOL = "AskUserQuestion";
|
|
828
|
+
var ABORTED_REASONS = /* @__PURE__ */ new Set(["aborted_streaming", "aborted_tools"]);
|
|
829
|
+
var STOPPED_REASONS = /* @__PURE__ */ new Set([
|
|
830
|
+
"max_turns",
|
|
831
|
+
"budget_exhausted",
|
|
832
|
+
"stop_hook_prevented",
|
|
833
|
+
"hook_stopped",
|
|
834
|
+
"tool_deferred",
|
|
835
|
+
"tool_deferred_unavailable",
|
|
836
|
+
"background_requested"
|
|
837
|
+
]);
|
|
838
|
+
function turnOutcome(terminalReason, subtype, interruptRequested) {
|
|
839
|
+
if (terminalReason && ABORTED_REASONS.has(terminalReason))
|
|
840
|
+
return "interrupted";
|
|
841
|
+
if (terminalReason && STOPPED_REASONS.has(terminalReason))
|
|
842
|
+
return "stopped";
|
|
843
|
+
if (subtype === "success")
|
|
844
|
+
return "completed";
|
|
845
|
+
if (interruptRequested)
|
|
846
|
+
return "interrupted";
|
|
847
|
+
if (subtype === "error_max_turns" || subtype === "error_max_budget_usd")
|
|
848
|
+
return "stopped";
|
|
849
|
+
return "failed";
|
|
850
|
+
}
|
|
650
851
|
function parseQuestions(input) {
|
|
651
852
|
if (typeof input !== "object" || input === null)
|
|
652
853
|
return null;
|
|
@@ -698,16 +899,54 @@ function previewAnnotations(questions, answers) {
|
|
|
698
899
|
return annotations;
|
|
699
900
|
}
|
|
700
901
|
var USAGE_MIN_INTERVAL_MS = 2e4;
|
|
902
|
+
var CONTEXT_MIN_INTERVAL_MS = 5e3;
|
|
701
903
|
var ClaudeSession = class extends EventEmitter {
|
|
702
904
|
id;
|
|
703
905
|
workspaceId;
|
|
704
906
|
cwd;
|
|
705
907
|
journal = new Journal();
|
|
706
908
|
#query = null;
|
|
909
|
+
/**
|
|
910
|
+
* Разговор, который был до нас, — прочитанный транскрипт продолженной сессии.
|
|
911
|
+
*
|
|
912
|
+
* Читается один раз, на старте, и больше не меняется: всё, что случится
|
|
913
|
+
* дальше, попадает в журнал. Промис, а не массив, потому что чтение
|
|
914
|
+
* асинхронное, а спросить историю могут раньше, чем оно закончится, — тогда
|
|
915
|
+
* запрос дождётся того же чтения вместо второго такого же. `null` — сессия
|
|
916
|
+
* начата с нуля, и продолжать нечего.
|
|
917
|
+
*/
|
|
918
|
+
#history = null;
|
|
707
919
|
/** Чем подняли эту сессию — нужно, чтобы объяснить провал старта. */
|
|
708
920
|
#executable = { source: "bundled" };
|
|
709
921
|
#inbox = new AsyncQueue();
|
|
922
|
+
/**
|
|
923
|
+
* Размер контекста на последнем запросе к API — то, что показывает полоса
|
|
924
|
+
* контекста в UI.
|
|
925
|
+
*
|
|
926
|
+
* Считается здесь, а не в UI из `result.usage`: SDK складывает в него все
|
|
927
|
+
* запросы хода разом, и разделить их обратно уже нечем. Usage
|
|
928
|
+
* `assistant`-сообщения, наоборот, всегда про один запрос — запоминаем
|
|
929
|
+
* последнее и отдаём его вместе с `result`.
|
|
930
|
+
*/
|
|
931
|
+
#contextTokens = null;
|
|
710
932
|
#state = "starting";
|
|
933
|
+
/**
|
|
934
|
+
* Чем кончился последний ход — см. `TurnOutcome` в протоколе.
|
|
935
|
+
*
|
|
936
|
+
* Обновляется на `result`, обнуляется новым промптом: свежий ход ещё ничем не
|
|
937
|
+
* кончился, и прошлый исход рядом с ним читался бы как исход этого.
|
|
938
|
+
*/
|
|
939
|
+
#lastTurn = null;
|
|
940
|
+
/**
|
|
941
|
+
* Человек нажал «Стоп» — до того, как пришёл `result` этого хода.
|
|
942
|
+
*
|
|
943
|
+
* Запасной путь к «прервали»: обычно исход виден по `terminal_reason`
|
|
944
|
+
* (`aborted_streaming`/`aborted_tools`, проверено пробой), но поле
|
|
945
|
+
* необязательное, а версия CLI живёт отдельно от нашей. Тогда решает флаг:
|
|
946
|
+
* если мы сами только что просили прервать, оборвавшийся ход — прерванный, а
|
|
947
|
+
* не упавший.
|
|
948
|
+
*/
|
|
949
|
+
#interruptRequested = false;
|
|
711
950
|
#capabilities = null;
|
|
712
951
|
#pendingPermissions = /* @__PURE__ */ new Map();
|
|
713
952
|
#model = null;
|
|
@@ -772,6 +1011,11 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
772
1011
|
/** Когда была последняя попытка его получить — удачная или нет. */
|
|
773
1012
|
#usageAt = 0;
|
|
774
1013
|
#usageInFlight = false;
|
|
1014
|
+
/** Последний снимок заполненности окна контекста; null — ещё не спрашивали. */
|
|
1015
|
+
#context = null;
|
|
1016
|
+
/** Когда была последняя попытка его получить — удачная или нет. */
|
|
1017
|
+
#contextAt = 0;
|
|
1018
|
+
#contextInFlight = false;
|
|
775
1019
|
#createdAt = Date.now();
|
|
776
1020
|
#updatedAt = Date.now();
|
|
777
1021
|
#options;
|
|
@@ -806,6 +1050,20 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
806
1050
|
get usage() {
|
|
807
1051
|
return this.#usage;
|
|
808
1052
|
}
|
|
1053
|
+
/** Заполненность окна контекста. null — ещё ни разу не спросили или спросить не вышло. */
|
|
1054
|
+
get context() {
|
|
1055
|
+
return this.#context;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Разговор, который был до старта этой сессии, — целиком и по порядку.
|
|
1059
|
+
*
|
|
1060
|
+
* Пустой массив у сессии, начатой с нуля. Ждёт чтения транскрипта, если оно
|
|
1061
|
+
* ещё идёт: клиент спрашивает историю сразу после открытия сессии, и ответ
|
|
1062
|
+
* «пока пусто» он принял бы за «истории нет».
|
|
1063
|
+
*/
|
|
1064
|
+
async history() {
|
|
1065
|
+
return await this.#history ?? [];
|
|
1066
|
+
}
|
|
809
1067
|
summary() {
|
|
810
1068
|
return {
|
|
811
1069
|
id: this.id,
|
|
@@ -815,6 +1073,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
815
1073
|
parentSessionId: this.parentSessionId,
|
|
816
1074
|
title: this.#title,
|
|
817
1075
|
state: this.#state,
|
|
1076
|
+
lastTurn: this.#lastTurn,
|
|
818
1077
|
model: this.#model,
|
|
819
1078
|
permissionMode: this.#permissionMode,
|
|
820
1079
|
effort: this.#effort,
|
|
@@ -833,6 +1092,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
833
1092
|
start() {
|
|
834
1093
|
if (this.#query)
|
|
835
1094
|
throw new Error("session already started");
|
|
1095
|
+
if (this.#options.resume && !this.#options.fork) {
|
|
1096
|
+
const until = Date.now();
|
|
1097
|
+
this.#history = readTranscript(this.#options.resume, { dir: this.cwd, until });
|
|
1098
|
+
}
|
|
836
1099
|
const options2 = {
|
|
837
1100
|
cwd: this.cwd,
|
|
838
1101
|
// Без этого SDK не читает с диска ничего: ни CLAUDE.md, ни кастомные
|
|
@@ -942,6 +1205,8 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
942
1205
|
});
|
|
943
1206
|
return;
|
|
944
1207
|
}
|
|
1208
|
+
this.#lastTurn = null;
|
|
1209
|
+
this.#interruptRequested = false;
|
|
945
1210
|
this.#setState("running");
|
|
946
1211
|
this.#inbox.push({
|
|
947
1212
|
type: "user",
|
|
@@ -988,6 +1253,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
988
1253
|
this.#setState("running");
|
|
989
1254
|
}
|
|
990
1255
|
async interrupt() {
|
|
1256
|
+
this.#interruptRequested = true;
|
|
991
1257
|
await this.#query?.interrupt();
|
|
992
1258
|
}
|
|
993
1259
|
/**
|
|
@@ -1090,6 +1356,44 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1090
1356
|
this.#usageInFlight = false;
|
|
1091
1357
|
}
|
|
1092
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Пересчитать заполненность окна контекста.
|
|
1361
|
+
*
|
|
1362
|
+
* Спрашиваем у CLI, а не считаем сами: `contextTokens` из `result` знает
|
|
1363
|
+
* только про разговор и только на конец хода, а в окне лежат ещё системный
|
|
1364
|
+
* промпт, инструменты, память и навыки. Здесь же приезжает и размер самого
|
|
1365
|
+
* окна — знаменатель полосы.
|
|
1366
|
+
*
|
|
1367
|
+
* `force` — по прямой просьбе клиента (открыли сессию, нажали «обновить»):
|
|
1368
|
+
* тогда пауза между запросами не действует.
|
|
1369
|
+
*
|
|
1370
|
+
* Неудача, как и у лимитов, ничего не ломает и в ленту не пишется: на CLI
|
|
1371
|
+
* постарше метода может не быть вовсе, и сессия от этого не перестаёт
|
|
1372
|
+
* работать — просто полоса останется на числе из последнего хода.
|
|
1373
|
+
*/
|
|
1374
|
+
async refreshContext(force = false) {
|
|
1375
|
+
const q = this.#query;
|
|
1376
|
+
if (!q || this.#contextInFlight)
|
|
1377
|
+
return;
|
|
1378
|
+
if (!force && Date.now() - this.#contextAt < CONTEXT_MIN_INTERVAL_MS)
|
|
1379
|
+
return;
|
|
1380
|
+
this.#contextInFlight = true;
|
|
1381
|
+
try {
|
|
1382
|
+
const raw = await q.getContextUsage();
|
|
1383
|
+
if (typeof raw.totalTokens === "number" && raw.maxTokens > 0) {
|
|
1384
|
+
this.#context = {
|
|
1385
|
+
tokens: Math.max(0, raw.totalTokens),
|
|
1386
|
+
maxTokens: raw.maxTokens,
|
|
1387
|
+
fetchedAt: Date.now()
|
|
1388
|
+
};
|
|
1389
|
+
this.emit("context", this.#context);
|
|
1390
|
+
}
|
|
1391
|
+
} catch {
|
|
1392
|
+
} finally {
|
|
1393
|
+
this.#contextAt = Date.now();
|
|
1394
|
+
this.#contextInFlight = false;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1093
1397
|
// -------------------------------------------------------------------------
|
|
1094
1398
|
// MCP и плагины
|
|
1095
1399
|
// -------------------------------------------------------------------------
|
|
@@ -1194,7 +1498,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1194
1498
|
const pass = { continue: true };
|
|
1195
1499
|
if (!this.workspaceId)
|
|
1196
1500
|
return pass;
|
|
1197
|
-
const roots = this.#options.writableRoots
|
|
1501
|
+
const roots = this.#options.writableRoots ? this.#options.writableRoots() : [this.cwd];
|
|
1198
1502
|
if (roots === null)
|
|
1199
1503
|
return pass;
|
|
1200
1504
|
const target = writeTarget(input.tool_input);
|
|
@@ -1306,6 +1610,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1306
1610
|
this.#handleSystem(message);
|
|
1307
1611
|
break;
|
|
1308
1612
|
case "assistant":
|
|
1613
|
+
if (message.parent_tool_use_id === null && message.message.usage) {
|
|
1614
|
+
const usage = message.message.usage;
|
|
1615
|
+
this.#contextTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
1616
|
+
}
|
|
1309
1617
|
for (const block of message.message.content) {
|
|
1310
1618
|
const blockId = `${message.message.id}:${this.#nextBlockIndex(message.message.id)}`;
|
|
1311
1619
|
if (block.type === "text") {
|
|
@@ -1349,6 +1657,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1349
1657
|
durationMs: message.duration_ms,
|
|
1350
1658
|
costUsd: message.total_cost_usd ?? null,
|
|
1351
1659
|
usage,
|
|
1660
|
+
contextTokens: this.#contextTokens,
|
|
1352
1661
|
text: "result" in message ? message.result : null
|
|
1353
1662
|
});
|
|
1354
1663
|
if (this.#options.outputFormat) {
|
|
@@ -1357,10 +1666,16 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1357
1666
|
text: "result" in message ? message.result : null
|
|
1358
1667
|
});
|
|
1359
1668
|
}
|
|
1669
|
+
this.#lastTurn = turnOutcome("terminal_reason" in message ? message.terminal_reason : void 0, message.subtype, this.#interruptRequested);
|
|
1670
|
+
this.#interruptRequested = false;
|
|
1671
|
+
const alreadyIdle = this.#state === "idle";
|
|
1360
1672
|
this.#setState("idle");
|
|
1673
|
+
if (alreadyIdle)
|
|
1674
|
+
this.emit("summary", this.summary());
|
|
1361
1675
|
if (this.#unpersistedTitle)
|
|
1362
1676
|
void this.#persistTitle();
|
|
1363
1677
|
void this.refreshUsage();
|
|
1678
|
+
void this.refreshContext(true);
|
|
1364
1679
|
break;
|
|
1365
1680
|
}
|
|
1366
1681
|
// CLI сам сообщил, что лимит сдвинулся (обычно — подошёл к порогу).
|
|
@@ -1399,6 +1714,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1399
1714
|
if (this.#state === "starting")
|
|
1400
1715
|
this.#setState("idle");
|
|
1401
1716
|
void this.refreshUsage(true);
|
|
1717
|
+
void this.refreshContext(true);
|
|
1402
1718
|
return;
|
|
1403
1719
|
}
|
|
1404
1720
|
const { type: _type, subtype, ...rest } = message;
|
|
@@ -1470,7 +1786,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1470
1786
|
this.#record({
|
|
1471
1787
|
kind: "tool.result",
|
|
1472
1788
|
toolUseId: block.tool_use_id,
|
|
1473
|
-
content:
|
|
1789
|
+
content: contentToText2(block.content),
|
|
1474
1790
|
isError: block.is_error === true
|
|
1475
1791
|
});
|
|
1476
1792
|
}
|
|
@@ -1551,7 +1867,7 @@ function toUsageWindow(raw) {
|
|
|
1551
1867
|
resetsAt: typeof raw.resets_at === "string" ? raw.resets_at : ""
|
|
1552
1868
|
};
|
|
1553
1869
|
}
|
|
1554
|
-
function
|
|
1870
|
+
function contentToText2(content) {
|
|
1555
1871
|
if (typeof content === "string")
|
|
1556
1872
|
return content;
|
|
1557
1873
|
if (!Array.isArray(content))
|
|
@@ -1932,9 +2248,9 @@ function parsePatch(raw) {
|
|
|
1932
2248
|
body = "";
|
|
1933
2249
|
}
|
|
1934
2250
|
total += body.length;
|
|
1935
|
-
const
|
|
2251
|
+
const path17 = status === "deleted" ? oldPath : newPath || oldPath;
|
|
1936
2252
|
files.push({
|
|
1937
|
-
path:
|
|
2253
|
+
path: path17,
|
|
1938
2254
|
oldPath: status === "renamed" ? oldPath : "",
|
|
1939
2255
|
status,
|
|
1940
2256
|
added: count(body, "+"),
|
|
@@ -1946,10 +2262,10 @@ function parsePatch(raw) {
|
|
|
1946
2262
|
return { files, truncated };
|
|
1947
2263
|
}
|
|
1948
2264
|
function stripPrefix(value) {
|
|
1949
|
-
const
|
|
1950
|
-
if (
|
|
2265
|
+
const path17 = unquote(value.trim());
|
|
2266
|
+
if (path17 === "/dev/null")
|
|
1951
2267
|
return "";
|
|
1952
|
-
return
|
|
2268
|
+
return path17.replace(/^[ab]\//, "");
|
|
1953
2269
|
}
|
|
1954
2270
|
function count(body, mark) {
|
|
1955
2271
|
let n = 0;
|
|
@@ -2522,14 +2838,139 @@ function isRecord(value) {
|
|
|
2522
2838
|
return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
|
|
2523
2839
|
}
|
|
2524
2840
|
|
|
2525
|
-
// ../host/dist/
|
|
2841
|
+
// ../host/dist/pins.js
|
|
2526
2842
|
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
2527
|
-
import { homedir as homedir3 } from "node:os";
|
|
2528
2843
|
import path9 from "node:path";
|
|
2844
|
+
var SessionPins = class {
|
|
2845
|
+
#pinned = /* @__PURE__ */ new Map();
|
|
2846
|
+
#storePath;
|
|
2847
|
+
constructor(dataDir) {
|
|
2848
|
+
this.#storePath = path9.join(dataDir, "session-pins.json");
|
|
2849
|
+
}
|
|
2850
|
+
async load() {
|
|
2851
|
+
try {
|
|
2852
|
+
const raw = await readFile6(this.#storePath, "utf8");
|
|
2853
|
+
const parsed = JSON.parse(raw);
|
|
2854
|
+
this.#pinned = new Map(Object.entries(parsed.pinned ?? {}).filter(([, r]) => isRecord2(r)));
|
|
2855
|
+
} catch {
|
|
2856
|
+
this.#pinned = /* @__PURE__ */ new Map();
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
find(sessionId) {
|
|
2860
|
+
return this.#pinned.get(sessionId);
|
|
2861
|
+
}
|
|
2862
|
+
has(sessionId) {
|
|
2863
|
+
return this.#pinned.has(sessionId);
|
|
2864
|
+
}
|
|
2865
|
+
/** Проекты, в которых есть хоть что-то закреплённое, — по ним собирается панель. */
|
|
2866
|
+
workspaceIds() {
|
|
2867
|
+
return [...new Set([...this.#pinned.values()].map((r) => r.workspaceId))];
|
|
2868
|
+
}
|
|
2869
|
+
/** Закрепить или снять закрепление. Возвращает `true`, если что-то изменилось. */
|
|
2870
|
+
async set(workspaceId, sessionIds, pinned) {
|
|
2871
|
+
let changed = false;
|
|
2872
|
+
const at = Date.now();
|
|
2873
|
+
for (const id of sessionIds) {
|
|
2874
|
+
if (pinned) {
|
|
2875
|
+
if (this.#pinned.has(id))
|
|
2876
|
+
continue;
|
|
2877
|
+
this.#pinned.set(id, { workspaceId, at });
|
|
2878
|
+
} else if (!this.#pinned.delete(id)) {
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
changed = true;
|
|
2882
|
+
}
|
|
2883
|
+
if (changed)
|
|
2884
|
+
await this.#persist();
|
|
2885
|
+
return changed;
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Перенести закрепление с локального uuid на настоящий id Claude Code.
|
|
2889
|
+
*
|
|
2890
|
+
* Свежую сессию закрепляют до первого обмена — тогда её дискового id ещё не
|
|
2891
|
+
* существует, и в реестр попадает временный uuid. Как только id появился,
|
|
2892
|
+
* запись обязана переехать на него: под uuid сессию не найдёт ни список
|
|
2893
|
+
* (`#listSessions` ищет закрепление по дисковому id), ни следующий запуск
|
|
2894
|
+
* хоста — там этого uuid нет вовсе, и подметание стёрло бы закрепление
|
|
2895
|
+
* молча.
|
|
2896
|
+
*
|
|
2897
|
+
* Время закрепления переносится как есть: порядок в панели задан им, и
|
|
2898
|
+
* строка не должна прыгать в конец списка от того, что сессия наконец
|
|
2899
|
+
* дошла до `system/init`.
|
|
2900
|
+
*/
|
|
2901
|
+
async migrate(fromId, toId) {
|
|
2902
|
+
const record = this.#pinned.get(fromId);
|
|
2903
|
+
if (!record || fromId === toId)
|
|
2904
|
+
return false;
|
|
2905
|
+
this.#pinned.delete(fromId);
|
|
2906
|
+
if (!this.#pinned.has(toId))
|
|
2907
|
+
this.#pinned.set(toId, record);
|
|
2908
|
+
await this.#persist();
|
|
2909
|
+
return true;
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Забыть записи о сессиях, которых больше нет на диске.
|
|
2913
|
+
*
|
|
2914
|
+
* Зовётся оттуда же, откуда подметается архив, и делает то же самое: реестр
|
|
2915
|
+
* не должен копить ссылки на исчезнувшие транскрипты. `known` — все сессии
|
|
2916
|
+
* проекта, живые и с диска.
|
|
2917
|
+
*/
|
|
2918
|
+
async sweep(workspaceId, known) {
|
|
2919
|
+
let changed = false;
|
|
2920
|
+
for (const [id, record] of this.#pinned) {
|
|
2921
|
+
if (record.workspaceId !== workspaceId || known.has(id))
|
|
2922
|
+
continue;
|
|
2923
|
+
this.#pinned.delete(id);
|
|
2924
|
+
changed = true;
|
|
2925
|
+
}
|
|
2926
|
+
if (changed)
|
|
2927
|
+
await this.#persist();
|
|
2928
|
+
return changed;
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* Забыть всё закреплённое в проекте — при его закрытии.
|
|
2932
|
+
*
|
|
2933
|
+
* Закрытый проект не показывается нигде, и закреплённая сессия из него
|
|
2934
|
+
* висела бы в панели строкой, по клику на которую открывать нечего.
|
|
2935
|
+
*/
|
|
2936
|
+
async forgetWorkspace(workspaceId) {
|
|
2937
|
+
let changed = false;
|
|
2938
|
+
for (const [id, record] of this.#pinned) {
|
|
2939
|
+
if (record.workspaceId !== workspaceId)
|
|
2940
|
+
continue;
|
|
2941
|
+
this.#pinned.delete(id);
|
|
2942
|
+
changed = true;
|
|
2943
|
+
}
|
|
2944
|
+
if (changed)
|
|
2945
|
+
await this.#persist();
|
|
2946
|
+
return changed;
|
|
2947
|
+
}
|
|
2948
|
+
async #persist() {
|
|
2949
|
+
const file = { pinned: Object.fromEntries(this.#pinned) };
|
|
2950
|
+
await mkdir4(path9.dirname(this.#storePath), { recursive: true });
|
|
2951
|
+
await writeFile4(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
function isRecord2(value) {
|
|
2955
|
+
return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// ../host/dist/app-settings.js
|
|
2959
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
2960
|
+
import { homedir as homedir3 } from "node:os";
|
|
2961
|
+
import path10 from "node:path";
|
|
2529
2962
|
var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
2530
2963
|
var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
|
|
2531
2964
|
var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
|
|
2532
2965
|
var FOLDER_ACCESS = ["all", "projects", "project"];
|
|
2966
|
+
var PERMISSION_MODES = [
|
|
2967
|
+
"default",
|
|
2968
|
+
"acceptEdits",
|
|
2969
|
+
"plan",
|
|
2970
|
+
"dontAsk",
|
|
2971
|
+
"bypassPermissions",
|
|
2972
|
+
"auto"
|
|
2973
|
+
];
|
|
2533
2974
|
var DEFAULT_APP_SETTINGS = {
|
|
2534
2975
|
autoArchiveAfterMs: DEFAULT_AUTO_ARCHIVE_AFTER_MS,
|
|
2535
2976
|
// Строгий режим по умолчанию: молча писать в чужой проект приложение начнёт
|
|
@@ -2537,17 +2978,26 @@ var DEFAULT_APP_SETTINGS = {
|
|
|
2537
2978
|
folderAccess: "project",
|
|
2538
2979
|
// Выключено по умолчанию: сохранение — явное действие, пока человек сам не
|
|
2539
2980
|
// попросил иначе.
|
|
2540
|
-
autoSaveAfterPauseMs: 0
|
|
2981
|
+
autoSaveAfterPauseMs: 0,
|
|
2982
|
+
// Модель за пользователя не выбираем: пока он не сказал иначе, сессия
|
|
2983
|
+
// поднимается ровно с той, с которой её поднял бы консольный Claude Code.
|
|
2984
|
+
startModel: null,
|
|
2985
|
+
// Тот же режим, с которым сессия стартовала до появления этой настройки.
|
|
2986
|
+
startPermissionMode: "default",
|
|
2987
|
+
// Терминал по умолчанию закрыт — его включают, а не выключают: приложение,
|
|
2988
|
+
// открытое в браузере, не обязано с первого запуска уметь запускать на этой
|
|
2989
|
+
// машине что угодно.
|
|
2990
|
+
terminalEnabled: false
|
|
2541
2991
|
};
|
|
2542
2992
|
var AppSettingsStore = class {
|
|
2543
2993
|
#settings = { ...DEFAULT_APP_SETTINGS };
|
|
2544
2994
|
#storePath;
|
|
2545
2995
|
constructor(dataDir) {
|
|
2546
|
-
this.#storePath =
|
|
2996
|
+
this.#storePath = path10.join(dataDir, "app-settings.json");
|
|
2547
2997
|
}
|
|
2548
2998
|
async load() {
|
|
2549
2999
|
try {
|
|
2550
|
-
const raw = await
|
|
3000
|
+
const raw = await readFile7(this.#storePath, "utf8");
|
|
2551
3001
|
this.#settings = sanitize(JSON.parse(raw), DEFAULT_APP_SETTINGS);
|
|
2552
3002
|
} catch {
|
|
2553
3003
|
this.#settings = { ...DEFAULT_APP_SETTINGS };
|
|
@@ -2567,8 +3017,8 @@ var AppSettingsStore = class {
|
|
|
2567
3017
|
async update(patch) {
|
|
2568
3018
|
const next = sanitize(patch, this.#settings);
|
|
2569
3019
|
this.#settings = next;
|
|
2570
|
-
await
|
|
2571
|
-
await
|
|
3020
|
+
await mkdir5(path10.dirname(this.#storePath), { recursive: true });
|
|
3021
|
+
await writeFile5(this.#storePath, JSON.stringify(next, null, 2), "utf8");
|
|
2572
3022
|
return this.current;
|
|
2573
3023
|
}
|
|
2574
3024
|
};
|
|
@@ -2581,18 +3031,32 @@ function sanitize(value, fallback) {
|
|
|
2581
3031
|
if (typeof after === "number" && Number.isFinite(after)) {
|
|
2582
3032
|
result.autoArchiveAfterMs = after <= 0 ? 0 : Math.max(Math.round(after), MIN_AUTO_ARCHIVE_AFTER_MS);
|
|
2583
3033
|
}
|
|
2584
|
-
const
|
|
2585
|
-
if (typeof
|
|
2586
|
-
result.folderAccess =
|
|
3034
|
+
const access3 = record.folderAccess;
|
|
3035
|
+
if (typeof access3 === "string" && FOLDER_ACCESS.includes(access3)) {
|
|
3036
|
+
result.folderAccess = access3;
|
|
2587
3037
|
}
|
|
2588
3038
|
const autoSave = record.autoSaveAfterPauseMs;
|
|
2589
3039
|
if (typeof autoSave === "number" && Number.isFinite(autoSave)) {
|
|
2590
3040
|
result.autoSaveAfterPauseMs = autoSave <= 0 ? 0 : Math.max(Math.round(autoSave), MIN_AUTO_SAVE_AFTER_PAUSE_MS);
|
|
2591
3041
|
}
|
|
3042
|
+
if ("startModel" in record) {
|
|
3043
|
+
const model = record.startModel;
|
|
3044
|
+
if (model === null)
|
|
3045
|
+
result.startModel = null;
|
|
3046
|
+
else if (typeof model === "string")
|
|
3047
|
+
result.startModel = model.trim() || null;
|
|
3048
|
+
}
|
|
3049
|
+
const mode = record.startPermissionMode;
|
|
3050
|
+
if (typeof mode === "string" && PERMISSION_MODES.includes(mode)) {
|
|
3051
|
+
result.startPermissionMode = mode;
|
|
3052
|
+
}
|
|
3053
|
+
if (typeof record.terminalEnabled === "boolean") {
|
|
3054
|
+
result.terminalEnabled = record.terminalEnabled;
|
|
3055
|
+
}
|
|
2592
3056
|
return result;
|
|
2593
3057
|
}
|
|
2594
3058
|
function claudeHome() {
|
|
2595
|
-
return
|
|
3059
|
+
return path10.join(homedir3(), ".claude");
|
|
2596
3060
|
}
|
|
2597
3061
|
function writableRoots(mode, cwd, workspacePaths) {
|
|
2598
3062
|
if (mode === "all")
|
|
@@ -2602,7 +3066,7 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
2602
3066
|
return [...roots];
|
|
2603
3067
|
for (const workspace of [cwd, ...workspacePaths]) {
|
|
2604
3068
|
roots.add(workspace);
|
|
2605
|
-
const parent =
|
|
3069
|
+
const parent = path10.dirname(workspace);
|
|
2606
3070
|
if (parent !== workspace)
|
|
2607
3071
|
roots.add(parent);
|
|
2608
3072
|
}
|
|
@@ -2610,9 +3074,9 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
2610
3074
|
}
|
|
2611
3075
|
|
|
2612
3076
|
// ../host/dist/plugins.js
|
|
2613
|
-
import { readFile as
|
|
3077
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2614
3078
|
import { homedir as homedir4 } from "node:os";
|
|
2615
|
-
import
|
|
3079
|
+
import path11 from "node:path";
|
|
2616
3080
|
var TIMEOUT_MS2 = 12e4;
|
|
2617
3081
|
var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
|
|
2618
3082
|
var PluginCli = class {
|
|
@@ -2711,10 +3175,10 @@ async function readCatalog(markets, installed) {
|
|
|
2711
3175
|
return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
2712
3176
|
}
|
|
2713
3177
|
async function readMarketplace(market, installed) {
|
|
2714
|
-
const root = market.installLocation ||
|
|
3178
|
+
const root = market.installLocation || path11.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
|
|
2715
3179
|
let manifest;
|
|
2716
3180
|
try {
|
|
2717
|
-
const text = await
|
|
3181
|
+
const text = await readFile8(path11.join(root, ".claude-plugin", "marketplace.json"), "utf8");
|
|
2718
3182
|
manifest = JSON.parse(text);
|
|
2719
3183
|
} catch {
|
|
2720
3184
|
return [];
|
|
@@ -2925,8 +3389,8 @@ function describe3(error) {
|
|
|
2925
3389
|
|
|
2926
3390
|
// ../host/dist/relay-link.js
|
|
2927
3391
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
2928
|
-
import { readFile as
|
|
2929
|
-
import
|
|
3392
|
+
import { readFile as readFile9, writeFile as writeFile6, rm as rm2, mkdir as mkdir6 } from "node:fs/promises";
|
|
3393
|
+
import path12 from "node:path";
|
|
2930
3394
|
import { hostname } from "node:os";
|
|
2931
3395
|
import WebSocket from "ws";
|
|
2932
3396
|
|
|
@@ -2948,13 +3412,26 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2948
3412
|
#clients = /* @__PURE__ */ new Set();
|
|
2949
3413
|
#error = null;
|
|
2950
3414
|
#closed = false;
|
|
3415
|
+
/**
|
|
3416
|
+
* Право удалённого терминала — то, что нам сказал relay, и ничего больше.
|
|
3417
|
+
*
|
|
3418
|
+
* На диск не пишется намеренно, в отличие от токена сопряжения: правда о
|
|
3419
|
+
* праве живёт на relay, а кеш на диске означал бы, что хост, поднятый без
|
|
3420
|
+
* связи, сам себе разрешает то, что мог отозвать другой человек час назад.
|
|
3421
|
+
* Поэтому после каждого запуска право закрыто, пока relay не пришлёт
|
|
3422
|
+
* `shell.state`, — и это ничего не стоит: удалённых вкладок до связи с relay
|
|
3423
|
+
* всё равно нет. Обрыв связи состояние не гасит по обратной причине: гасить
|
|
3424
|
+
* нечего (вкладок снова нет), а мигание «выключено» в настройках от каждого
|
|
3425
|
+
* зевка сети означало бы звать человека чинить то, что чинится само.
|
|
3426
|
+
*/
|
|
3427
|
+
#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
|
|
2951
3428
|
/** Куда сопрягаться, пока не сопряглись. */
|
|
2952
3429
|
#defaultUrl;
|
|
2953
3430
|
#file;
|
|
2954
3431
|
#log;
|
|
2955
3432
|
constructor(options2) {
|
|
2956
3433
|
super();
|
|
2957
|
-
this.#file =
|
|
3434
|
+
this.#file = path12.join(options2.dataDir, "relay.json");
|
|
2958
3435
|
this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
|
|
2959
3436
|
this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
|
|
2960
3437
|
}
|
|
@@ -2969,9 +3446,57 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2969
3446
|
name: this.#credentials?.name ?? null,
|
|
2970
3447
|
hostId: this.#credentials?.hostId ?? null,
|
|
2971
3448
|
clients: this.#clients.size,
|
|
3449
|
+
shell: this.#shell,
|
|
2972
3450
|
error: this.#error
|
|
2973
3451
|
};
|
|
2974
3452
|
}
|
|
3453
|
+
/** Право удалённого терминала — им же гейтится каждый кадр `term.*` от удалённой вкладки. */
|
|
3454
|
+
get shell() {
|
|
3455
|
+
return this.#shell;
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Включить удалённый терминал прямо сейчас — вызывается только для запроса,
|
|
3459
|
+
* пришедшего с локального сокета.
|
|
3460
|
+
*
|
|
3461
|
+
* Решение «локально или удалённо» принимает `HostServer`, а не этот класс:
|
|
3462
|
+
* здесь уже нечем отличить одно от другого, а там это ровно то поле
|
|
3463
|
+
* `ClientChannel.local`, ради которого канал и заведён.
|
|
3464
|
+
*/
|
|
3465
|
+
async enableShell() {
|
|
3466
|
+
if (!this.#credentials)
|
|
3467
|
+
throw new Error("\u041A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440 \u043D\u0435 \u0441\u0432\u044F\u0437\u0430\u043D \u0441 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u043E\u043C \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435");
|
|
3468
|
+
if (this.#socket?.readyState !== WebSocket.OPEN) {
|
|
3469
|
+
throw new Error("\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438 \u0441 relay \u2014 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435 \u0432\u044B\u0439\u0434\u0435\u0442");
|
|
3470
|
+
}
|
|
3471
|
+
this.#push({ t: "shell.enable" });
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Попросить relay выслать письмо, подтверждающее удалённый терминал.
|
|
3475
|
+
*
|
|
3476
|
+
* Само право этот вызов не даёт и дать не может: relay только отправляет
|
|
3477
|
+
* письмо, а включает доступ переход по ссылке из него. Поэтому здесь нет ни
|
|
3478
|
+
* ожидания ответа, ни оптимистичного `enabled = true` — состояние приедет
|
|
3479
|
+
* кадром `shell.state`, когда будет чему приезжать.
|
|
3480
|
+
*/
|
|
3481
|
+
async requestShell() {
|
|
3482
|
+
if (!this.#credentials)
|
|
3483
|
+
throw new Error("\u041A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440 \u043D\u0435 \u0441\u0432\u044F\u0437\u0430\u043D \u0441 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u043E\u043C \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435");
|
|
3484
|
+
if (this.#socket?.readyState !== WebSocket.OPEN) {
|
|
3485
|
+
throw new Error("\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438 \u0441 relay \u2014 \u043F\u0438\u0441\u044C\u043C\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u043A\u043E\u043C\u0443, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0437\u0436\u0435");
|
|
3486
|
+
}
|
|
3487
|
+
this.#push({ t: "shell.request" });
|
|
3488
|
+
}
|
|
3489
|
+
/** Снять право немедленно. Тоже через relay: там оно и хранится. */
|
|
3490
|
+
async disableShell() {
|
|
3491
|
+
if (!this.#credentials)
|
|
3492
|
+
return;
|
|
3493
|
+
if (this.#socket?.readyState !== WebSocket.OPEN) {
|
|
3494
|
+
this.#shell = { ...this.#shell, enabled: false, pending: false };
|
|
3495
|
+
this.#changed();
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
this.#push({ t: "shell.disable" });
|
|
3499
|
+
}
|
|
2975
3500
|
#changed() {
|
|
2976
3501
|
this.emit("state");
|
|
2977
3502
|
}
|
|
@@ -2987,7 +3512,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2987
3512
|
}
|
|
2988
3513
|
async #read() {
|
|
2989
3514
|
try {
|
|
2990
|
-
const raw = await
|
|
3515
|
+
const raw = await readFile9(this.#file, "utf8");
|
|
2991
3516
|
const parsed = JSON.parse(raw);
|
|
2992
3517
|
if (typeof parsed !== "object" || parsed === null)
|
|
2993
3518
|
return null;
|
|
@@ -3000,8 +3525,8 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
3000
3525
|
}
|
|
3001
3526
|
}
|
|
3002
3527
|
async #write(credentials) {
|
|
3003
|
-
await
|
|
3004
|
-
await
|
|
3528
|
+
await mkdir6(path12.dirname(this.#file), { recursive: true });
|
|
3529
|
+
await writeFile6(this.#file, `${JSON.stringify(credentials, null, 2)}
|
|
3005
3530
|
`, {
|
|
3006
3531
|
encoding: "utf8",
|
|
3007
3532
|
mode: 384
|
|
@@ -3052,6 +3577,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
3052
3577
|
this.#credentials = null;
|
|
3053
3578
|
this.#error = null;
|
|
3054
3579
|
this.#clients.clear();
|
|
3580
|
+
this.#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
|
|
3055
3581
|
await rm2(this.#file, { force: true });
|
|
3056
3582
|
this.#changed();
|
|
3057
3583
|
this.#log("\u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435 \u0441\u043D\u044F\u0442\u043E");
|
|
@@ -3157,6 +3683,16 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
3157
3683
|
case "client.frame":
|
|
3158
3684
|
this.emit("frame", frame.clientId, frame.body);
|
|
3159
3685
|
return;
|
|
3686
|
+
case "shell.state":
|
|
3687
|
+
this.#shell = {
|
|
3688
|
+
enabled: frame.enabled,
|
|
3689
|
+
pending: frame.pending,
|
|
3690
|
+
email: frame.email,
|
|
3691
|
+
confirmedAt: frame.confirmedAt
|
|
3692
|
+
};
|
|
3693
|
+
this.#log(frame.enabled ? "\u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D" : frame.pending ? `\u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B: \u043F\u0438\u0441\u044C\u043C\u043E \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E \u043D\u0430 ${frame.email}` : "\u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D");
|
|
3694
|
+
this.#changed();
|
|
3695
|
+
return;
|
|
3160
3696
|
case "ping":
|
|
3161
3697
|
socket.send(JSON.stringify({ t: "pong", nonce: frame.nonce }));
|
|
3162
3698
|
return;
|
|
@@ -3197,11 +3733,196 @@ function describe4(error) {
|
|
|
3197
3733
|
return error instanceof Error ? error.message : String(error);
|
|
3198
3734
|
}
|
|
3199
3735
|
|
|
3736
|
+
// ../host/dist/terminal.js
|
|
3737
|
+
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
3738
|
+
import { access as access2, constants as constants2 } from "node:fs/promises";
|
|
3739
|
+
import path13 from "node:path";
|
|
3740
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3741
|
+
var SCROLLBACK_BYTES = 256 * 1024;
|
|
3742
|
+
var FLUSH_MS = 12;
|
|
3743
|
+
var MAX_TERMINALS = 24;
|
|
3744
|
+
var TerminalHub = class extends EventEmitter4 {
|
|
3745
|
+
#terminals = /* @__PURE__ */ new Map();
|
|
3746
|
+
#module = null;
|
|
3747
|
+
list(workspaceId) {
|
|
3748
|
+
const all = [...this.#terminals.values()].map((entry) => entry.info);
|
|
3749
|
+
return workspaceId ? all.filter((info) => info.workspaceId === workspaceId) : all;
|
|
3750
|
+
}
|
|
3751
|
+
has(id) {
|
|
3752
|
+
return this.#terminals.has(id);
|
|
3753
|
+
}
|
|
3754
|
+
/** Накопленный вывод — то, что увидит подключившийся позже. */
|
|
3755
|
+
buffer(id) {
|
|
3756
|
+
const entry = this.#terminals.get(id);
|
|
3757
|
+
if (!entry)
|
|
3758
|
+
return "";
|
|
3759
|
+
return entry.chunks.join("") + entry.pending;
|
|
3760
|
+
}
|
|
3761
|
+
async open(options2) {
|
|
3762
|
+
if (this.#terminals.size >= MAX_TERMINALS) {
|
|
3763
|
+
throw new Error(`\u041E\u0442\u043A\u0440\u044B\u0442\u043E \u0443\u0436\u0435 ${MAX_TERMINALS} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u043E\u0432 \u2014 \u0437\u0430\u043A\u0440\u043E\u0439\u0442\u0435 \u043B\u0438\u0448\u043D\u0438\u0435`);
|
|
3764
|
+
}
|
|
3765
|
+
const pty = await this.#pty();
|
|
3766
|
+
const shell = await defaultShell();
|
|
3767
|
+
const cols = clampSize(options2.cols, 20, 500, 80);
|
|
3768
|
+
const rows = clampSize(options2.rows, 5, 200, 24);
|
|
3769
|
+
const child = pty.spawn(shell.file, shell.args, {
|
|
3770
|
+
name: "xterm-256color",
|
|
3771
|
+
cols,
|
|
3772
|
+
rows,
|
|
3773
|
+
cwd: options2.cwd,
|
|
3774
|
+
env: environment()
|
|
3775
|
+
});
|
|
3776
|
+
const info = {
|
|
3777
|
+
id: randomUUID2(),
|
|
3778
|
+
workspaceId: options2.workspaceId,
|
|
3779
|
+
title: shell.title,
|
|
3780
|
+
shell: shell.file,
|
|
3781
|
+
cwd: options2.cwd,
|
|
3782
|
+
cols,
|
|
3783
|
+
rows,
|
|
3784
|
+
exitCode: null
|
|
3785
|
+
};
|
|
3786
|
+
const entry = { info, pty: child, chunks: [], chunksLength: 0, pending: "", flush: null };
|
|
3787
|
+
this.#terminals.set(info.id, entry);
|
|
3788
|
+
child.onData((data) => this.#onData(entry, data));
|
|
3789
|
+
child.onExit(({ exitCode, signal }) => {
|
|
3790
|
+
this.#flush(entry);
|
|
3791
|
+
entry.info.exitCode = exitCode;
|
|
3792
|
+
this.emit("exit", info.id, exitCode, signal ?? null);
|
|
3793
|
+
});
|
|
3794
|
+
return info;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Ввод человека — как есть.
|
|
3798
|
+
*
|
|
3799
|
+
* Ни разбора, ни экранирования, ни проверки: перед нами не «команда», а поток
|
|
3800
|
+
* нажатий, в котором Ctrl+C, Tab и вставка из буфера обмена неотличимы от
|
|
3801
|
+
* обычных букв и должны такими остаться.
|
|
3802
|
+
*/
|
|
3803
|
+
write(id, data) {
|
|
3804
|
+
const entry = this.#terminals.get(id);
|
|
3805
|
+
if (!entry)
|
|
3806
|
+
throw new Error("\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442");
|
|
3807
|
+
if (entry.info.exitCode !== null)
|
|
3808
|
+
return;
|
|
3809
|
+
entry.pty.write(data);
|
|
3810
|
+
}
|
|
3811
|
+
resize(id, cols, rows) {
|
|
3812
|
+
const entry = this.#terminals.get(id);
|
|
3813
|
+
if (!entry || entry.info.exitCode !== null)
|
|
3814
|
+
return;
|
|
3815
|
+
const next = { cols: clampSize(cols, 20, 500, 80), rows: clampSize(rows, 5, 200, 24) };
|
|
3816
|
+
if (next.cols === entry.info.cols && next.rows === entry.info.rows)
|
|
3817
|
+
return;
|
|
3818
|
+
entry.info.cols = next.cols;
|
|
3819
|
+
entry.info.rows = next.rows;
|
|
3820
|
+
try {
|
|
3821
|
+
entry.pty.resize(next.cols, next.rows);
|
|
3822
|
+
} catch {
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
close(id) {
|
|
3826
|
+
const entry = this.#terminals.get(id);
|
|
3827
|
+
if (!entry)
|
|
3828
|
+
return;
|
|
3829
|
+
this.#terminals.delete(id);
|
|
3830
|
+
if (entry.flush)
|
|
3831
|
+
clearTimeout(entry.flush);
|
|
3832
|
+
try {
|
|
3833
|
+
entry.pty.kill();
|
|
3834
|
+
} catch {
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
/** Погасить всё: незакрытые оболочки переживут хост и останутся висеть. */
|
|
3838
|
+
closeAll() {
|
|
3839
|
+
for (const id of [...this.#terminals.keys()])
|
|
3840
|
+
this.close(id);
|
|
3841
|
+
}
|
|
3842
|
+
#onData(entry, data) {
|
|
3843
|
+
entry.pending += data;
|
|
3844
|
+
if (entry.flush)
|
|
3845
|
+
return;
|
|
3846
|
+
entry.flush = setTimeout(() => this.#flush(entry), FLUSH_MS);
|
|
3847
|
+
entry.flush.unref?.();
|
|
3848
|
+
}
|
|
3849
|
+
#flush(entry) {
|
|
3850
|
+
if (entry.flush) {
|
|
3851
|
+
clearTimeout(entry.flush);
|
|
3852
|
+
entry.flush = null;
|
|
3853
|
+
}
|
|
3854
|
+
const data = entry.pending;
|
|
3855
|
+
if (!data)
|
|
3856
|
+
return;
|
|
3857
|
+
entry.pending = "";
|
|
3858
|
+
entry.chunks.push(data);
|
|
3859
|
+
entry.chunksLength += data.length;
|
|
3860
|
+
while (entry.chunksLength > SCROLLBACK_BYTES && entry.chunks.length > 1) {
|
|
3861
|
+
entry.chunksLength -= entry.chunks.shift().length;
|
|
3862
|
+
}
|
|
3863
|
+
this.emit("data", entry.info.id, data);
|
|
3864
|
+
}
|
|
3865
|
+
#pty() {
|
|
3866
|
+
this.#module ??= import("@lydell/node-pty").then((module) => module).catch((error) => {
|
|
3867
|
+
this.#module = null;
|
|
3868
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3869
|
+
throw new Error(`\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: ${reason}`);
|
|
3870
|
+
});
|
|
3871
|
+
return this.#module;
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
function clampSize(value, min, max, fallback) {
|
|
3875
|
+
if (!Number.isFinite(value))
|
|
3876
|
+
return fallback;
|
|
3877
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
3878
|
+
}
|
|
3879
|
+
function environment() {
|
|
3880
|
+
const env = {
|
|
3881
|
+
...process.env,
|
|
3882
|
+
TERM: "xterm-256color",
|
|
3883
|
+
COLORTERM: "truecolor"
|
|
3884
|
+
};
|
|
3885
|
+
for (const key of Object.keys(env))
|
|
3886
|
+
if (key.startsWith("="))
|
|
3887
|
+
delete env[key];
|
|
3888
|
+
return env;
|
|
3889
|
+
}
|
|
3890
|
+
async function defaultShell() {
|
|
3891
|
+
if (process.platform === "win32") {
|
|
3892
|
+
const pwsh = await findOnPath("pwsh.exe");
|
|
3893
|
+
if (pwsh)
|
|
3894
|
+
return { file: pwsh, args: [], title: "pwsh" };
|
|
3895
|
+
const system = process.env.SystemRoot ?? "C:\\Windows";
|
|
3896
|
+
const powershell = path13.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
|
|
3897
|
+
if (await exists2(powershell))
|
|
3898
|
+
return { file: powershell, args: [], title: "powershell" };
|
|
3899
|
+
return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
|
|
3900
|
+
}
|
|
3901
|
+
const shell = process.env.SHELL;
|
|
3902
|
+
if (shell && await exists2(shell))
|
|
3903
|
+
return { file: shell, args: [], title: path13.basename(shell) };
|
|
3904
|
+
if (await exists2("/bin/bash"))
|
|
3905
|
+
return { file: "/bin/bash", args: [], title: "bash" };
|
|
3906
|
+
return { file: "/bin/sh", args: [], title: "sh" };
|
|
3907
|
+
}
|
|
3908
|
+
async function findOnPath(name) {
|
|
3909
|
+
const dirs = (process.env.PATH ?? "").split(path13.delimiter).filter(Boolean);
|
|
3910
|
+
for (const dir of dirs) {
|
|
3911
|
+
const candidate = path13.join(dir, name);
|
|
3912
|
+
if (await exists2(candidate))
|
|
3913
|
+
return candidate;
|
|
3914
|
+
}
|
|
3915
|
+
return null;
|
|
3916
|
+
}
|
|
3917
|
+
function exists2(file) {
|
|
3918
|
+
return access2(file, constants2.X_OK).then(() => true, () => false);
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3200
3921
|
// ../host/dist/mcp-config.js
|
|
3201
|
-
import { readFile as
|
|
3202
|
-
import
|
|
3922
|
+
import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7 } from "node:fs/promises";
|
|
3923
|
+
import path14 from "node:path";
|
|
3203
3924
|
var MCP_FILE = ".mcp.json";
|
|
3204
|
-
var LOCAL_SETTINGS2 =
|
|
3925
|
+
var LOCAL_SETTINGS2 = path14.join(".claude", "settings.local.json");
|
|
3205
3926
|
var queues = /* @__PURE__ */ new Map();
|
|
3206
3927
|
function serialize(cwd, work) {
|
|
3207
3928
|
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
@@ -3210,7 +3931,7 @@ function serialize(cwd, work) {
|
|
|
3210
3931
|
return next;
|
|
3211
3932
|
}
|
|
3212
3933
|
async function readProjectServers(cwd) {
|
|
3213
|
-
const file = await readJson(
|
|
3934
|
+
const file = await readJson(path14.join(cwd, MCP_FILE));
|
|
3214
3935
|
const servers = file?.mcpServers;
|
|
3215
3936
|
if (!servers || typeof servers !== "object")
|
|
3216
3937
|
return {};
|
|
@@ -3218,23 +3939,23 @@ async function readProjectServers(cwd) {
|
|
|
3218
3939
|
}
|
|
3219
3940
|
function addProjectServer(cwd, name, config) {
|
|
3220
3941
|
return serialize(cwd, async () => {
|
|
3221
|
-
const file = await readJson(
|
|
3942
|
+
const file = await readJson(path14.join(cwd, MCP_FILE)) ?? {};
|
|
3222
3943
|
const servers = file.mcpServers ?? {};
|
|
3223
3944
|
servers[name] = config;
|
|
3224
3945
|
file.mcpServers = servers;
|
|
3225
|
-
await writeJson(
|
|
3946
|
+
await writeJson(path14.join(cwd, MCP_FILE), file);
|
|
3226
3947
|
await approve(cwd, name);
|
|
3227
3948
|
});
|
|
3228
3949
|
}
|
|
3229
3950
|
function removeProjectServer(cwd, name) {
|
|
3230
3951
|
return serialize(cwd, async () => {
|
|
3231
|
-
const file = await readJson(
|
|
3952
|
+
const file = await readJson(path14.join(cwd, MCP_FILE));
|
|
3232
3953
|
const servers = file?.mcpServers;
|
|
3233
3954
|
if (!servers || !(name in servers))
|
|
3234
3955
|
return false;
|
|
3235
3956
|
delete servers[name];
|
|
3236
|
-
await writeJson(
|
|
3237
|
-
const settingsPath =
|
|
3957
|
+
await writeJson(path14.join(cwd, MCP_FILE), file);
|
|
3958
|
+
const settingsPath = path14.join(cwd, LOCAL_SETTINGS2);
|
|
3238
3959
|
const settings = await readJson(settingsPath);
|
|
3239
3960
|
const enabled = settings?.enabledMcpjsonServers;
|
|
3240
3961
|
if (settings && Array.isArray(enabled) && enabled.includes(name)) {
|
|
@@ -3245,7 +3966,7 @@ function removeProjectServer(cwd, name) {
|
|
|
3245
3966
|
});
|
|
3246
3967
|
}
|
|
3247
3968
|
async function approve(cwd, name) {
|
|
3248
|
-
const file =
|
|
3969
|
+
const file = path14.join(cwd, LOCAL_SETTINGS2);
|
|
3249
3970
|
const settings = await readJson(file) ?? {};
|
|
3250
3971
|
const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
3251
3972
|
if (!enabled.includes(name))
|
|
@@ -3284,31 +4005,34 @@ function describeConfig(config) {
|
|
|
3284
4005
|
}
|
|
3285
4006
|
async function readJson(file) {
|
|
3286
4007
|
try {
|
|
3287
|
-
const raw = await
|
|
4008
|
+
const raw = await readFile10(file, "utf8");
|
|
3288
4009
|
const parsed = JSON.parse(raw);
|
|
3289
4010
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
3290
4011
|
} catch (error) {
|
|
3291
4012
|
if (error.code === "ENOENT")
|
|
3292
4013
|
return null;
|
|
3293
4014
|
if (error instanceof SyntaxError) {
|
|
3294
|
-
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${
|
|
4015
|
+
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path14.basename(file)}: ${error.message}`);
|
|
3295
4016
|
}
|
|
3296
4017
|
throw error;
|
|
3297
4018
|
}
|
|
3298
4019
|
}
|
|
3299
4020
|
async function writeJson(file, data) {
|
|
3300
|
-
await
|
|
3301
|
-
await
|
|
4021
|
+
await mkdir7(path14.dirname(file), { recursive: true });
|
|
4022
|
+
await writeFile7(file, `${JSON.stringify(data, null, 2)}
|
|
3302
4023
|
`, "utf8");
|
|
3303
4024
|
}
|
|
3304
4025
|
|
|
3305
4026
|
// ../host/dist/server.js
|
|
3306
4027
|
var SIDE_TITLE = "\xB7 \u0432\u043E\u043F\u0440\u043E\u0441 \u043D\u0430 \u043F\u043E\u043B\u044F\u0445";
|
|
3307
4028
|
var FINDER_TITLE = "\xB7 \u043F\u043E\u0438\u0441\u043A \u043F\u0440\u043E\u0435\u043A\u0442\u0430";
|
|
4029
|
+
var HISTORY_PAGE = 250;
|
|
4030
|
+
var HISTORY_PAGE_MAX = 500;
|
|
3308
4031
|
var HostServer = class {
|
|
3309
4032
|
#options;
|
|
3310
4033
|
#workspaces;
|
|
3311
4034
|
#archive;
|
|
4035
|
+
#pins;
|
|
3312
4036
|
#settings;
|
|
3313
4037
|
#finder;
|
|
3314
4038
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -3326,18 +4050,21 @@ var HostServer = class {
|
|
|
3326
4050
|
#clients = /* @__PURE__ */ new Set();
|
|
3327
4051
|
/** Удалённые вкладки по их id в relay — чтобы адресовать ответ именно той. */
|
|
3328
4052
|
#remotes = /* @__PURE__ */ new Map();
|
|
3329
|
-
#hostId =
|
|
4053
|
+
#hostId = randomUUID3();
|
|
3330
4054
|
#plugins = new PluginCli();
|
|
3331
4055
|
#auth = new AuthCli();
|
|
3332
4056
|
#relay;
|
|
4057
|
+
#terminals = new TerminalHub();
|
|
3333
4058
|
constructor(options2) {
|
|
3334
4059
|
this.#options = options2;
|
|
3335
4060
|
this.#workspaces = new WorkspaceRegistry(options2.dataDir);
|
|
3336
4061
|
this.#archive = new SessionArchive(options2.dataDir);
|
|
4062
|
+
this.#pins = new SessionPins(options2.dataDir);
|
|
3337
4063
|
this.#settings = new AppSettingsStore(options2.dataDir);
|
|
3338
4064
|
this.#finder = new ProjectFinder(this.#workspaces);
|
|
3339
4065
|
this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
|
|
3340
4066
|
this.#wireRelay();
|
|
4067
|
+
this.#wireTerminals();
|
|
3341
4068
|
this.#auth.on("login", (state) => {
|
|
3342
4069
|
this.#broadcast({ type: "auth.login", state });
|
|
3343
4070
|
});
|
|
@@ -3348,6 +4075,7 @@ var HostServer = class {
|
|
|
3348
4075
|
async start() {
|
|
3349
4076
|
await this.#workspaces.load();
|
|
3350
4077
|
await this.#archive.load();
|
|
4078
|
+
await this.#pins.load();
|
|
3351
4079
|
await this.#settings.load();
|
|
3352
4080
|
await warmClaudeLauncher();
|
|
3353
4081
|
void this.#finder.warm();
|
|
@@ -3373,6 +4101,7 @@ var HostServer = class {
|
|
|
3373
4101
|
}
|
|
3374
4102
|
async shutdown() {
|
|
3375
4103
|
this.#auth.cancel();
|
|
4104
|
+
this.#terminals.closeAll();
|
|
3376
4105
|
await this.#relay.close();
|
|
3377
4106
|
await Promise.all([...this.#sessions.values()].map((s) => s.close("host shutdown")));
|
|
3378
4107
|
this.#sessions.clear();
|
|
@@ -3436,6 +4165,36 @@ var HostServer = class {
|
|
|
3436
4165
|
this.#broadcast({ type: "relay.state", state: this.#relay.state });
|
|
3437
4166
|
});
|
|
3438
4167
|
}
|
|
4168
|
+
// -------------------------------------------------------------------------
|
|
4169
|
+
// Терминал
|
|
4170
|
+
// -------------------------------------------------------------------------
|
|
4171
|
+
#wireTerminals() {
|
|
4172
|
+
this.#terminals.on("data", (id, data) => {
|
|
4173
|
+
this.#broadcastTerminal({ type: "term.data", id, data });
|
|
4174
|
+
});
|
|
4175
|
+
this.#terminals.on("exit", (id, exitCode, signal) => {
|
|
4176
|
+
this.#broadcastTerminal({ type: "term.exit", id, exitCode, signal });
|
|
4177
|
+
});
|
|
4178
|
+
}
|
|
4179
|
+
/**
|
|
4180
|
+
* Вывод терминала — всем локальным вкладкам, удалённым только по праву.
|
|
4181
|
+
*
|
|
4182
|
+
* Отдельно от `#broadcast` именно поэтому: обычный broadcast уходит и в
|
|
4183
|
+
* relay, а вывод оболочки — то самое, что удалённой стороне видеть не
|
|
4184
|
+
* положено, пока доступ не подтверждён письмом. Ошибиться здесь дороже, чем
|
|
4185
|
+
* в проверке входящего кадра: вход можно не заметить и отклонить, а вывод,
|
|
4186
|
+
* ушедший наружу, уже ушёл.
|
|
4187
|
+
*/
|
|
4188
|
+
#broadcastTerminal(frame) {
|
|
4189
|
+
if (this.#relay.shell.enabled) {
|
|
4190
|
+
this.#broadcast(frame);
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
for (const client of this.#clients) {
|
|
4194
|
+
if (client.local)
|
|
4195
|
+
client.send(frame);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
3439
4198
|
#adoptRemote(clientId) {
|
|
3440
4199
|
const client = {
|
|
3441
4200
|
local: false,
|
|
@@ -3509,10 +4268,17 @@ var HostServer = class {
|
|
|
3509
4268
|
continue;
|
|
3510
4269
|
await this.#dropSession(session.id, "workspace closed");
|
|
3511
4270
|
}
|
|
4271
|
+
let unpinned = false;
|
|
4272
|
+
for (const id of gone) {
|
|
4273
|
+
if (await this.#pins.forgetWorkspace(id))
|
|
4274
|
+
unpinned = true;
|
|
4275
|
+
}
|
|
3512
4276
|
this.#broadcast({
|
|
3513
4277
|
type: "workspace.list",
|
|
3514
4278
|
workspaces: this.#workspaces.list()
|
|
3515
4279
|
});
|
|
4280
|
+
if (unpinned)
|
|
4281
|
+
await this.#sendPinned();
|
|
3516
4282
|
return;
|
|
3517
4283
|
}
|
|
3518
4284
|
case "workspace.find": {
|
|
@@ -3575,6 +4341,27 @@ var HostServer = class {
|
|
|
3575
4341
|
await this.#archive.unarchive(frame.sessionIds);
|
|
3576
4342
|
await this.#sendSessionList(frame.workspaceId);
|
|
3577
4343
|
return;
|
|
4344
|
+
case "session.pin": {
|
|
4345
|
+
const ids = new Set(frame.sessionIds);
|
|
4346
|
+
for (const session of [...this.#sessions.values()]) {
|
|
4347
|
+
const diskId = session.claudeSessionId;
|
|
4348
|
+
if (!diskId)
|
|
4349
|
+
continue;
|
|
4350
|
+
if (ids.has(session.id))
|
|
4351
|
+
ids.add(diskId);
|
|
4352
|
+
if (ids.has(diskId))
|
|
4353
|
+
ids.add(session.id);
|
|
4354
|
+
}
|
|
4355
|
+
await this.#pins.set(frame.workspaceId, [...ids], frame.pinned);
|
|
4356
|
+
const hadPins = this.#pins.workspaceIds().includes(frame.workspaceId);
|
|
4357
|
+
await this.#sendSessionList(frame.workspaceId);
|
|
4358
|
+
if (!hadPins)
|
|
4359
|
+
await this.#sendPinned();
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
case "session.pins":
|
|
4363
|
+
await this.#sendPinned();
|
|
4364
|
+
return;
|
|
3578
4365
|
case "session.rename":
|
|
3579
4366
|
await this.#renameSession(frame.sessionId, frame.title, frame.workspaceId);
|
|
3580
4367
|
return;
|
|
@@ -3596,11 +4383,33 @@ var HostServer = class {
|
|
|
3596
4383
|
usage: session.usage
|
|
3597
4384
|
});
|
|
3598
4385
|
}
|
|
4386
|
+
if (session.context) {
|
|
4387
|
+
this.#send(socket, {
|
|
4388
|
+
type: "context.state",
|
|
4389
|
+
sessionId: session.id,
|
|
4390
|
+
context: session.context
|
|
4391
|
+
});
|
|
4392
|
+
}
|
|
3599
4393
|
if (entries.length > 0) {
|
|
3600
4394
|
this.#send(socket, { type: "session.events", sessionId: session.id, entries });
|
|
3601
4395
|
}
|
|
3602
4396
|
return;
|
|
3603
4397
|
}
|
|
4398
|
+
case "session.history": {
|
|
4399
|
+
const session = this.#requireSession(frame.sessionId);
|
|
4400
|
+
const all = await session.history();
|
|
4401
|
+
const end = typeof frame.before === "number" ? Math.max(0, Math.min(all.length, frame.before - 1)) : all.length;
|
|
4402
|
+
const limit = Math.min(Math.max(frame.limit ?? HISTORY_PAGE, 1), HISTORY_PAGE_MAX);
|
|
4403
|
+
const start = Math.max(0, end - limit);
|
|
4404
|
+
this.#send(socket, {
|
|
4405
|
+
type: "session.history",
|
|
4406
|
+
sessionId: session.id,
|
|
4407
|
+
entries: all.slice(start, end),
|
|
4408
|
+
from: start + 1,
|
|
4409
|
+
total: all.length
|
|
4410
|
+
});
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
3604
4413
|
case "usage.get": {
|
|
3605
4414
|
const session = this.#requireSession(frame.sessionId);
|
|
3606
4415
|
if (session.usage) {
|
|
@@ -3613,6 +4422,18 @@ var HostServer = class {
|
|
|
3613
4422
|
await session.refreshUsage(true);
|
|
3614
4423
|
return;
|
|
3615
4424
|
}
|
|
4425
|
+
case "context.get": {
|
|
4426
|
+
const session = this.#requireSession(frame.sessionId);
|
|
4427
|
+
if (session.context) {
|
|
4428
|
+
this.#send(socket, {
|
|
4429
|
+
type: "context.state",
|
|
4430
|
+
sessionId: session.id,
|
|
4431
|
+
context: session.context
|
|
4432
|
+
});
|
|
4433
|
+
}
|
|
4434
|
+
await session.refreshContext(true);
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
3616
4437
|
case "prompt.send":
|
|
3617
4438
|
this.#requireSession(frame.sessionId).sendPrompt(frame.text);
|
|
3618
4439
|
return;
|
|
@@ -3822,6 +4643,54 @@ var HostServer = class {
|
|
|
3822
4643
|
}
|
|
3823
4644
|
return;
|
|
3824
4645
|
}
|
|
4646
|
+
// Терминал. Каждая ручка начинается с одной и той же проверки права: их
|
|
4647
|
+
// пять, и «забыл в одной» здесь означает не потерянную кнопку, а чужую
|
|
4648
|
+
// оболочку в руках у того, кому её не давали.
|
|
4649
|
+
case "term.list":
|
|
4650
|
+
this.#requireShellAccess(socket);
|
|
4651
|
+
this.#send(socket, {
|
|
4652
|
+
type: "term.list",
|
|
4653
|
+
workspaceId: frame.workspaceId,
|
|
4654
|
+
terminals: this.#terminals.list(frame.workspaceId)
|
|
4655
|
+
});
|
|
4656
|
+
return;
|
|
4657
|
+
case "term.open": {
|
|
4658
|
+
this.#requireShellAccess(socket);
|
|
4659
|
+
const workspace = this.#workspaces.find(frame.workspaceId);
|
|
4660
|
+
if (!workspace)
|
|
4661
|
+
throw new Error(`\u0412\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${frame.workspaceId}`);
|
|
4662
|
+
const terminal = await this.#terminals.open({
|
|
4663
|
+
workspaceId: workspace.id,
|
|
4664
|
+
cwd: workspace.path,
|
|
4665
|
+
cols: frame.cols,
|
|
4666
|
+
rows: frame.rows
|
|
4667
|
+
});
|
|
4668
|
+
this.#broadcastTerminal({ type: "term.opened", terminal });
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4671
|
+
case "term.attach":
|
|
4672
|
+
this.#requireShellAccess(socket);
|
|
4673
|
+
if (!this.#terminals.has(frame.id))
|
|
4674
|
+
throw new Error("\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442");
|
|
4675
|
+
this.#send(socket, {
|
|
4676
|
+
type: "term.buffer",
|
|
4677
|
+
id: frame.id,
|
|
4678
|
+
data: this.#terminals.buffer(frame.id)
|
|
4679
|
+
});
|
|
4680
|
+
return;
|
|
4681
|
+
case "term.input":
|
|
4682
|
+
this.#requireShellAccess(socket);
|
|
4683
|
+
this.#terminals.write(frame.id, frame.data);
|
|
4684
|
+
return;
|
|
4685
|
+
case "term.resize":
|
|
4686
|
+
this.#requireShellAccess(socket);
|
|
4687
|
+
this.#terminals.resize(frame.id, frame.cols, frame.rows);
|
|
4688
|
+
return;
|
|
4689
|
+
case "term.close":
|
|
4690
|
+
this.#requireShellAccess(socket);
|
|
4691
|
+
this.#terminals.close(frame.id);
|
|
4692
|
+
this.#broadcastTerminal({ type: "term.closed", id: frame.id });
|
|
4693
|
+
return;
|
|
3825
4694
|
case "settings.get":
|
|
3826
4695
|
this.#send(socket, { type: "settings.state", settings: this.#settings.current });
|
|
3827
4696
|
return;
|
|
@@ -3834,6 +4703,12 @@ var HostServer = class {
|
|
|
3834
4703
|
await this.#sendSessionList(workspace.id);
|
|
3835
4704
|
}
|
|
3836
4705
|
}
|
|
4706
|
+
if (before.terminalEnabled && !settings.terminalEnabled) {
|
|
4707
|
+
for (const terminal of this.#terminals.list()) {
|
|
4708
|
+
this.#terminals.close(terminal.id);
|
|
4709
|
+
this.#broadcastTerminal({ type: "term.closed", id: terminal.id });
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
3837
4712
|
return;
|
|
3838
4713
|
}
|
|
3839
4714
|
case "auth.status":
|
|
@@ -3863,7 +4738,40 @@ var HostServer = class {
|
|
|
3863
4738
|
this.#requireLocal(socket, "\u041E\u0442\u0432\u044F\u0437\u0430\u0442\u044C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440");
|
|
3864
4739
|
await this.#relay.unpair();
|
|
3865
4740
|
return;
|
|
4741
|
+
case "relay.shell.enable":
|
|
4742
|
+
if (!this.#settings.current.terminalEnabled) {
|
|
4743
|
+
throw new Error("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u2014 \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439 \u0434\u043E\u0441\u0442\u0443\u043F \u0434\u0430\u0451\u0442\u0441\u044F \u043A \u043D\u0435\u043C\u0443, \u0430 \u0435\u0433\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442.");
|
|
4744
|
+
}
|
|
4745
|
+
if (socket.local)
|
|
4746
|
+
await this.#relay.enableShell();
|
|
4747
|
+
else
|
|
4748
|
+
await this.#relay.requestShell();
|
|
4749
|
+
return;
|
|
4750
|
+
case "relay.shell.disable":
|
|
4751
|
+
await this.#relay.disableShell();
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
/**
|
|
4756
|
+
* Терминал — по двум условиям сразу: включён ли он вообще и, для удалённой
|
|
4757
|
+
* вкладки, подтверждено ли право.
|
|
4758
|
+
*
|
|
4759
|
+
* Первое — настройка самой машины (`AppSettings.terminalEnabled`), выключена
|
|
4760
|
+
* по умолчанию: пока её не подняли, терминала нет ни у кого. Второе, поверх
|
|
4761
|
+
* первого: локальному клиенту не отказываем никогда — он и так за этой
|
|
4762
|
+
* машиной, где терминал открывается и без нас. Удалённому — по состоянию,
|
|
4763
|
+
* которое ведёт
|
|
4764
|
+
* relay: право включают либо на самой машине (сразу, физический доступ и
|
|
4765
|
+
* есть подтверждение), либо из удалённой вкладки, и тогда его включает не
|
|
4766
|
+
* нажатие кнопки, а переход по ссылке из письма на почту аккаунта.
|
|
4767
|
+
*/
|
|
4768
|
+
#requireShellAccess(client) {
|
|
4769
|
+
if (!this.#settings.current.terminalEnabled) {
|
|
4770
|
+
throw new Error("\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D. \u0412\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F, \u0440\u0430\u0437\u0434\u0435\u043B \xAB\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\xBB.");
|
|
3866
4771
|
}
|
|
4772
|
+
if (client.local || this.#relay.shell.enabled)
|
|
4773
|
+
return;
|
|
4774
|
+
throw new Error("\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0441 \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0445 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D. \u0412\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u043E\u0433\u043E \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u2014 \u043D\u0430 \u0441\u0430\u043C\u043E\u043C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\u0435 \u043E\u043D \u0432\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443, \u043E\u0442\u0441\u044E\u0434\u0430 \u043F\u043E\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0441\u0441\u044B\u043B\u043A\u043E\u0439 \u0438\u0437 \u043F\u0438\u0441\u044C\u043C\u0430.");
|
|
3867
4775
|
}
|
|
3868
4776
|
/**
|
|
3869
4777
|
* Настройки удалённого доступа меняются только с самой машины.
|
|
@@ -3997,6 +4905,32 @@ var HostServer = class {
|
|
|
3997
4905
|
async #sendSessionList(workspaceId) {
|
|
3998
4906
|
const { sessions, archived } = await this.#listSessions(workspaceId);
|
|
3999
4907
|
this.#broadcast({ type: "session.list", workspaceId, sessions, archived });
|
|
4908
|
+
if (this.#pins.workspaceIds().includes(workspaceId))
|
|
4909
|
+
await this.#sendPinned();
|
|
4910
|
+
}
|
|
4911
|
+
/**
|
|
4912
|
+
* Разослать закреплённые сессии — по всем проектам сразу.
|
|
4913
|
+
*
|
|
4914
|
+
* Собирается из тех же списков, что и панель проекта (`#listSessions`), а не
|
|
4915
|
+
* из одного реестра закреплений: в панели закреплённых у строки должны быть
|
|
4916
|
+
* имя, состояние и время последнего события, а реестр знает только id. Обход
|
|
4917
|
+
* идёт лишь по проектам, где вообще что-то закреплено, — их обычно единицы, и
|
|
4918
|
+
* читать диск по всем открытым проектам ради этого незачем.
|
|
4919
|
+
*
|
|
4920
|
+
* Архивные сюда тоже попадают: закреплённое в архив само не уезжает, но
|
|
4921
|
+
* убрать туда руками уже закреплённую сессию человеку никто не мешает, и
|
|
4922
|
+
* терять её из панели после этого было бы странно.
|
|
4923
|
+
*/
|
|
4924
|
+
async #sendPinned() {
|
|
4925
|
+
const sessions = [];
|
|
4926
|
+
for (const workspaceId of this.#pins.workspaceIds()) {
|
|
4927
|
+
if (!this.#workspaces.find(workspaceId))
|
|
4928
|
+
continue;
|
|
4929
|
+
const { sessions: live, archived } = await this.#listSessions(workspaceId);
|
|
4930
|
+
sessions.push(...[...live, ...archived].filter((s) => s.pinned));
|
|
4931
|
+
}
|
|
4932
|
+
sessions.sort((a, b) => (a.pinnedAt ?? 0) - (b.pinnedAt ?? 0));
|
|
4933
|
+
this.#broadcast({ type: "session.pinned", sessions });
|
|
4000
4934
|
}
|
|
4001
4935
|
async #startSession(socket, frame) {
|
|
4002
4936
|
const workspace = this.#workspaces.find(frame.workspaceId);
|
|
@@ -4006,13 +4940,14 @@ var HostServer = class {
|
|
|
4006
4940
|
const resumedTitle = resumed ? diskTitle(resumed) : null;
|
|
4007
4941
|
if (frame.resume)
|
|
4008
4942
|
await this.#archive.unarchive([frame.resume]);
|
|
4943
|
+
const startDefaults = this.#settings.current;
|
|
4009
4944
|
const session = new ClaudeSession({
|
|
4010
4945
|
workspaceId: workspace.id,
|
|
4011
4946
|
cwd: workspace.path,
|
|
4012
4947
|
resume: frame.resume,
|
|
4013
4948
|
fork: frame.fork,
|
|
4014
|
-
permissionMode: frame.permissionMode,
|
|
4015
|
-
model: frame.model,
|
|
4949
|
+
permissionMode: frame.permissionMode ?? startDefaults.startPermissionMode,
|
|
4950
|
+
model: frame.model ?? startDefaults.startModel ?? void 0,
|
|
4016
4951
|
...resumedTitle ? { title: resumedTitle } : {},
|
|
4017
4952
|
// Имя проекта вместо «Без названия»: свежую сессию в списке уже надо
|
|
4018
4953
|
// как-то называть, а сказать о ней пока нечего, кроме того, где она.
|
|
@@ -4036,6 +4971,9 @@ var HostServer = class {
|
|
|
4036
4971
|
});
|
|
4037
4972
|
session.on("summary", (summary) => {
|
|
4038
4973
|
this.#broadcast({ type: "session.summary", session: summary });
|
|
4974
|
+
if (!summary.claudeSessionId || !this.#pins.has(summary.id))
|
|
4975
|
+
return;
|
|
4976
|
+
void this.#pins.migrate(summary.id, summary.claudeSessionId).then((moved) => moved ? this.#sendPinned() : void 0).catch(() => void 0);
|
|
4039
4977
|
});
|
|
4040
4978
|
session.on("capabilities", () => {
|
|
4041
4979
|
if (!session.capabilities)
|
|
@@ -4049,6 +4987,9 @@ var HostServer = class {
|
|
|
4049
4987
|
session.on("usage", (usage) => {
|
|
4050
4988
|
this.#broadcast({ type: "usage.state", sessionId: session.id, usage });
|
|
4051
4989
|
});
|
|
4990
|
+
session.on("context", (context) => {
|
|
4991
|
+
this.#broadcast({ type: "context.state", sessionId: session.id, context });
|
|
4992
|
+
});
|
|
4052
4993
|
session.on("stderr", (data) => {
|
|
4053
4994
|
process.stderr.write(`[claude ${session.id.slice(0, 8)}] ${data}`);
|
|
4054
4995
|
});
|
|
@@ -4298,6 +5239,9 @@ var HostServer = class {
|
|
|
4298
5239
|
parentSessionId: null,
|
|
4299
5240
|
title: diskTitle(info),
|
|
4300
5241
|
state: "closed",
|
|
5242
|
+
// Хода в этой жизни у неё не было: сессия не поднята, и чем кончился
|
|
5243
|
+
// последний ход прошлой, знает только её JSONL.
|
|
5244
|
+
lastTurn: null,
|
|
4301
5245
|
model: null,
|
|
4302
5246
|
permissionMode: "default",
|
|
4303
5247
|
// Сессия с диска не поднята: чем она была настроена, знает только её
|
|
@@ -4311,10 +5255,14 @@ var HostServer = class {
|
|
|
4311
5255
|
lastSeq: 0,
|
|
4312
5256
|
totalCostUsd: 0
|
|
4313
5257
|
}));
|
|
4314
|
-
|
|
5258
|
+
const known = /* @__PURE__ */ new Set([...liveIds, ...historical.map((s) => s.id)]);
|
|
5259
|
+
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);
|
|
5260
|
+
await this.#pins.sweep(workspaceId, known);
|
|
4315
5261
|
const sessions = [];
|
|
4316
5262
|
const archived = [];
|
|
4317
|
-
for (const
|
|
5263
|
+
for (const base of [...live, ...historical].sort((a, b) => b.updatedAt - a.updatedAt)) {
|
|
5264
|
+
const pin = this.#pins.find(base.claudeSessionId ?? base.id);
|
|
5265
|
+
const summary = pin ? { ...base, pinned: true, pinnedAt: pin.at } : base;
|
|
4318
5266
|
const record = this.#archive.find(summary.claudeSessionId ?? summary.id);
|
|
4319
5267
|
if (!record) {
|
|
4320
5268
|
sessions.push(summary);
|
|
@@ -4352,8 +5300,8 @@ var HostServer = class {
|
|
|
4352
5300
|
return;
|
|
4353
5301
|
}
|
|
4354
5302
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
4355
|
-
const requested =
|
|
4356
|
-
const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested :
|
|
5303
|
+
const requested = path15.join(webRoot, path15.normalize(url2.pathname));
|
|
5304
|
+
const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested : path15.join(webRoot, "index.html");
|
|
4357
5305
|
if (!existsSync3(target)) {
|
|
4358
5306
|
res.writeHead(404).end("not found");
|
|
4359
5307
|
return;
|
|
@@ -4379,12 +5327,12 @@ function contentType(file) {
|
|
|
4379
5327
|
".svg": "image/svg+xml",
|
|
4380
5328
|
".woff2": "font/woff2"
|
|
4381
5329
|
};
|
|
4382
|
-
return types[
|
|
5330
|
+
return types[path15.extname(file)] ?? "application/octet-stream";
|
|
4383
5331
|
}
|
|
4384
5332
|
|
|
4385
5333
|
// dist/ccv.js
|
|
4386
|
-
var HERE =
|
|
4387
|
-
var PACKAGE_ROOT =
|
|
5334
|
+
var HERE = path16.dirname(fileURLToPath(import.meta.url));
|
|
5335
|
+
var PACKAGE_ROOT = path16.resolve(HERE, "..");
|
|
4388
5336
|
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.
|
|
4389
5337
|
|
|
4390
5338
|
\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
|
|
@@ -4451,7 +5399,7 @@ var server = new HostServer({
|
|
|
4451
5399
|
dataDir: defaults.dataDir,
|
|
4452
5400
|
// UI лежит в пакете рядом с бандлом — это и есть разница между установленной
|
|
4453
5401
|
// утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
|
|
4454
|
-
webRoot: process.env.CCV_WEB_ROOT ??
|
|
5402
|
+
webRoot: process.env.CCV_WEB_ROOT ?? path16.join(PACKAGE_ROOT, "web"),
|
|
4455
5403
|
version,
|
|
4456
5404
|
relayUrl: defaults.relayUrl
|
|
4457
5405
|
});
|
|
@@ -4480,7 +5428,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
4480
5428
|
}
|
|
4481
5429
|
async function packageVersion() {
|
|
4482
5430
|
try {
|
|
4483
|
-
const text = await
|
|
5431
|
+
const text = await readFile11(path16.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
4484
5432
|
return JSON.parse(text).version ?? "0.0.0";
|
|
4485
5433
|
} catch {
|
|
4486
5434
|
return "0.0.0";
|