@i14ks/ccv 0.2.0 → 0.3.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 +936 -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-DNwOY4Od.js +1 -0
- package/web/assets/index--X3c59oq.js +7 -0
- package/web/assets/index-1vcRYewd.js +1 -0
- package/web/assets/index-6JwHpeCk.js +1 -0
- package/web/assets/index-891wE-Rj.js +7 -0
- package/web/assets/index-9FU2NRHn.js +13 -0
- package/web/assets/index-9FstbMxt.js +1 -0
- package/web/assets/{index-1iYnLw7q.js → index-AptXdWK3.js} +1 -1
- package/web/assets/index-BFn7R550.js +1 -0
- package/web/assets/index-BFoyQq0x.js +1 -0
- package/web/assets/index-BHot-lnj.js +5 -0
- package/web/assets/index-Bh-gxu6Z.js +1 -0
- package/web/assets/index-Bj3bZRgw.js +2 -0
- package/web/assets/index-BvcLb1wh.js +1 -0
- package/web/assets/index-C9MQTTvT.js +1 -0
- package/web/assets/{index-D-buiUaW.js → index-CKclwUJ-.js} +2 -2
- package/web/assets/index-Cfccn-Ob.js +1 -0
- package/web/assets/index-Cvz4rtnD.js +1 -0
- package/web/assets/{index-C9vrRfhW.js → index-CzlH_Mh9.js} +3 -3
- package/web/assets/index-D9UhxKGI.js +126 -0
- package/web/assets/index-DB9ZVtJ8.js +1 -0
- package/web/assets/index-DmJcZf8t.css +1 -0
- package/web/assets/index-DtcfXsIX.js +3 -0
- package/web/assets/index-r40StboH.js +1 -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;
|
|
@@ -704,10 +905,47 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
704
905
|
cwd;
|
|
705
906
|
journal = new Journal();
|
|
706
907
|
#query = null;
|
|
908
|
+
/**
|
|
909
|
+
* Разговор, который был до нас, — прочитанный транскрипт продолженной сессии.
|
|
910
|
+
*
|
|
911
|
+
* Читается один раз, на старте, и больше не меняется: всё, что случится
|
|
912
|
+
* дальше, попадает в журнал. Промис, а не массив, потому что чтение
|
|
913
|
+
* асинхронное, а спросить историю могут раньше, чем оно закончится, — тогда
|
|
914
|
+
* запрос дождётся того же чтения вместо второго такого же. `null` — сессия
|
|
915
|
+
* начата с нуля, и продолжать нечего.
|
|
916
|
+
*/
|
|
917
|
+
#history = null;
|
|
707
918
|
/** Чем подняли эту сессию — нужно, чтобы объяснить провал старта. */
|
|
708
919
|
#executable = { source: "bundled" };
|
|
709
920
|
#inbox = new AsyncQueue();
|
|
921
|
+
/**
|
|
922
|
+
* Размер контекста на последнем запросе к API — то, что показывает полоса
|
|
923
|
+
* контекста в UI.
|
|
924
|
+
*
|
|
925
|
+
* Считается здесь, а не в UI из `result.usage`: SDK складывает в него все
|
|
926
|
+
* запросы хода разом, и разделить их обратно уже нечем. Usage
|
|
927
|
+
* `assistant`-сообщения, наоборот, всегда про один запрос — запоминаем
|
|
928
|
+
* последнее и отдаём его вместе с `result`.
|
|
929
|
+
*/
|
|
930
|
+
#contextTokens = null;
|
|
710
931
|
#state = "starting";
|
|
932
|
+
/**
|
|
933
|
+
* Чем кончился последний ход — см. `TurnOutcome` в протоколе.
|
|
934
|
+
*
|
|
935
|
+
* Обновляется на `result`, обнуляется новым промптом: свежий ход ещё ничем не
|
|
936
|
+
* кончился, и прошлый исход рядом с ним читался бы как исход этого.
|
|
937
|
+
*/
|
|
938
|
+
#lastTurn = null;
|
|
939
|
+
/**
|
|
940
|
+
* Человек нажал «Стоп» — до того, как пришёл `result` этого хода.
|
|
941
|
+
*
|
|
942
|
+
* Запасной путь к «прервали»: обычно исход виден по `terminal_reason`
|
|
943
|
+
* (`aborted_streaming`/`aborted_tools`, проверено пробой), но поле
|
|
944
|
+
* необязательное, а версия CLI живёт отдельно от нашей. Тогда решает флаг:
|
|
945
|
+
* если мы сами только что просили прервать, оборвавшийся ход — прерванный, а
|
|
946
|
+
* не упавший.
|
|
947
|
+
*/
|
|
948
|
+
#interruptRequested = false;
|
|
711
949
|
#capabilities = null;
|
|
712
950
|
#pendingPermissions = /* @__PURE__ */ new Map();
|
|
713
951
|
#model = null;
|
|
@@ -806,6 +1044,16 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
806
1044
|
get usage() {
|
|
807
1045
|
return this.#usage;
|
|
808
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Разговор, который был до старта этой сессии, — целиком и по порядку.
|
|
1049
|
+
*
|
|
1050
|
+
* Пустой массив у сессии, начатой с нуля. Ждёт чтения транскрипта, если оно
|
|
1051
|
+
* ещё идёт: клиент спрашивает историю сразу после открытия сессии, и ответ
|
|
1052
|
+
* «пока пусто» он принял бы за «истории нет».
|
|
1053
|
+
*/
|
|
1054
|
+
async history() {
|
|
1055
|
+
return await this.#history ?? [];
|
|
1056
|
+
}
|
|
809
1057
|
summary() {
|
|
810
1058
|
return {
|
|
811
1059
|
id: this.id,
|
|
@@ -815,6 +1063,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
815
1063
|
parentSessionId: this.parentSessionId,
|
|
816
1064
|
title: this.#title,
|
|
817
1065
|
state: this.#state,
|
|
1066
|
+
lastTurn: this.#lastTurn,
|
|
818
1067
|
model: this.#model,
|
|
819
1068
|
permissionMode: this.#permissionMode,
|
|
820
1069
|
effort: this.#effort,
|
|
@@ -833,6 +1082,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
833
1082
|
start() {
|
|
834
1083
|
if (this.#query)
|
|
835
1084
|
throw new Error("session already started");
|
|
1085
|
+
if (this.#options.resume && !this.#options.fork) {
|
|
1086
|
+
const until = Date.now();
|
|
1087
|
+
this.#history = readTranscript(this.#options.resume, { dir: this.cwd, until });
|
|
1088
|
+
}
|
|
836
1089
|
const options2 = {
|
|
837
1090
|
cwd: this.cwd,
|
|
838
1091
|
// Без этого SDK не читает с диска ничего: ни CLAUDE.md, ни кастомные
|
|
@@ -942,6 +1195,8 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
942
1195
|
});
|
|
943
1196
|
return;
|
|
944
1197
|
}
|
|
1198
|
+
this.#lastTurn = null;
|
|
1199
|
+
this.#interruptRequested = false;
|
|
945
1200
|
this.#setState("running");
|
|
946
1201
|
this.#inbox.push({
|
|
947
1202
|
type: "user",
|
|
@@ -988,6 +1243,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
988
1243
|
this.#setState("running");
|
|
989
1244
|
}
|
|
990
1245
|
async interrupt() {
|
|
1246
|
+
this.#interruptRequested = true;
|
|
991
1247
|
await this.#query?.interrupt();
|
|
992
1248
|
}
|
|
993
1249
|
/**
|
|
@@ -1194,7 +1450,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1194
1450
|
const pass = { continue: true };
|
|
1195
1451
|
if (!this.workspaceId)
|
|
1196
1452
|
return pass;
|
|
1197
|
-
const roots = this.#options.writableRoots
|
|
1453
|
+
const roots = this.#options.writableRoots ? this.#options.writableRoots() : [this.cwd];
|
|
1198
1454
|
if (roots === null)
|
|
1199
1455
|
return pass;
|
|
1200
1456
|
const target = writeTarget(input.tool_input);
|
|
@@ -1306,6 +1562,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1306
1562
|
this.#handleSystem(message);
|
|
1307
1563
|
break;
|
|
1308
1564
|
case "assistant":
|
|
1565
|
+
if (message.parent_tool_use_id === null && message.message.usage) {
|
|
1566
|
+
const usage = message.message.usage;
|
|
1567
|
+
this.#contextTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
1568
|
+
}
|
|
1309
1569
|
for (const block of message.message.content) {
|
|
1310
1570
|
const blockId = `${message.message.id}:${this.#nextBlockIndex(message.message.id)}`;
|
|
1311
1571
|
if (block.type === "text") {
|
|
@@ -1349,6 +1609,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1349
1609
|
durationMs: message.duration_ms,
|
|
1350
1610
|
costUsd: message.total_cost_usd ?? null,
|
|
1351
1611
|
usage,
|
|
1612
|
+
contextTokens: this.#contextTokens,
|
|
1352
1613
|
text: "result" in message ? message.result : null
|
|
1353
1614
|
});
|
|
1354
1615
|
if (this.#options.outputFormat) {
|
|
@@ -1357,7 +1618,12 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1357
1618
|
text: "result" in message ? message.result : null
|
|
1358
1619
|
});
|
|
1359
1620
|
}
|
|
1621
|
+
this.#lastTurn = turnOutcome("terminal_reason" in message ? message.terminal_reason : void 0, message.subtype, this.#interruptRequested);
|
|
1622
|
+
this.#interruptRequested = false;
|
|
1623
|
+
const alreadyIdle = this.#state === "idle";
|
|
1360
1624
|
this.#setState("idle");
|
|
1625
|
+
if (alreadyIdle)
|
|
1626
|
+
this.emit("summary", this.summary());
|
|
1361
1627
|
if (this.#unpersistedTitle)
|
|
1362
1628
|
void this.#persistTitle();
|
|
1363
1629
|
void this.refreshUsage();
|
|
@@ -1470,7 +1736,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1470
1736
|
this.#record({
|
|
1471
1737
|
kind: "tool.result",
|
|
1472
1738
|
toolUseId: block.tool_use_id,
|
|
1473
|
-
content:
|
|
1739
|
+
content: contentToText2(block.content),
|
|
1474
1740
|
isError: block.is_error === true
|
|
1475
1741
|
});
|
|
1476
1742
|
}
|
|
@@ -1551,7 +1817,7 @@ function toUsageWindow(raw) {
|
|
|
1551
1817
|
resetsAt: typeof raw.resets_at === "string" ? raw.resets_at : ""
|
|
1552
1818
|
};
|
|
1553
1819
|
}
|
|
1554
|
-
function
|
|
1820
|
+
function contentToText2(content) {
|
|
1555
1821
|
if (typeof content === "string")
|
|
1556
1822
|
return content;
|
|
1557
1823
|
if (!Array.isArray(content))
|
|
@@ -1932,9 +2198,9 @@ function parsePatch(raw) {
|
|
|
1932
2198
|
body = "";
|
|
1933
2199
|
}
|
|
1934
2200
|
total += body.length;
|
|
1935
|
-
const
|
|
2201
|
+
const path17 = status === "deleted" ? oldPath : newPath || oldPath;
|
|
1936
2202
|
files.push({
|
|
1937
|
-
path:
|
|
2203
|
+
path: path17,
|
|
1938
2204
|
oldPath: status === "renamed" ? oldPath : "",
|
|
1939
2205
|
status,
|
|
1940
2206
|
added: count(body, "+"),
|
|
@@ -1946,10 +2212,10 @@ function parsePatch(raw) {
|
|
|
1946
2212
|
return { files, truncated };
|
|
1947
2213
|
}
|
|
1948
2214
|
function stripPrefix(value) {
|
|
1949
|
-
const
|
|
1950
|
-
if (
|
|
2215
|
+
const path17 = unquote(value.trim());
|
|
2216
|
+
if (path17 === "/dev/null")
|
|
1951
2217
|
return "";
|
|
1952
|
-
return
|
|
2218
|
+
return path17.replace(/^[ab]\//, "");
|
|
1953
2219
|
}
|
|
1954
2220
|
function count(body, mark) {
|
|
1955
2221
|
let n = 0;
|
|
@@ -2522,14 +2788,139 @@ function isRecord(value) {
|
|
|
2522
2788
|
return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
|
|
2523
2789
|
}
|
|
2524
2790
|
|
|
2525
|
-
// ../host/dist/
|
|
2791
|
+
// ../host/dist/pins.js
|
|
2526
2792
|
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
2527
|
-
import { homedir as homedir3 } from "node:os";
|
|
2528
2793
|
import path9 from "node:path";
|
|
2794
|
+
var SessionPins = class {
|
|
2795
|
+
#pinned = /* @__PURE__ */ new Map();
|
|
2796
|
+
#storePath;
|
|
2797
|
+
constructor(dataDir) {
|
|
2798
|
+
this.#storePath = path9.join(dataDir, "session-pins.json");
|
|
2799
|
+
}
|
|
2800
|
+
async load() {
|
|
2801
|
+
try {
|
|
2802
|
+
const raw = await readFile6(this.#storePath, "utf8");
|
|
2803
|
+
const parsed = JSON.parse(raw);
|
|
2804
|
+
this.#pinned = new Map(Object.entries(parsed.pinned ?? {}).filter(([, r]) => isRecord2(r)));
|
|
2805
|
+
} catch {
|
|
2806
|
+
this.#pinned = /* @__PURE__ */ new Map();
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
find(sessionId) {
|
|
2810
|
+
return this.#pinned.get(sessionId);
|
|
2811
|
+
}
|
|
2812
|
+
has(sessionId) {
|
|
2813
|
+
return this.#pinned.has(sessionId);
|
|
2814
|
+
}
|
|
2815
|
+
/** Проекты, в которых есть хоть что-то закреплённое, — по ним собирается панель. */
|
|
2816
|
+
workspaceIds() {
|
|
2817
|
+
return [...new Set([...this.#pinned.values()].map((r) => r.workspaceId))];
|
|
2818
|
+
}
|
|
2819
|
+
/** Закрепить или снять закрепление. Возвращает `true`, если что-то изменилось. */
|
|
2820
|
+
async set(workspaceId, sessionIds, pinned) {
|
|
2821
|
+
let changed = false;
|
|
2822
|
+
const at = Date.now();
|
|
2823
|
+
for (const id of sessionIds) {
|
|
2824
|
+
if (pinned) {
|
|
2825
|
+
if (this.#pinned.has(id))
|
|
2826
|
+
continue;
|
|
2827
|
+
this.#pinned.set(id, { workspaceId, at });
|
|
2828
|
+
} else if (!this.#pinned.delete(id)) {
|
|
2829
|
+
continue;
|
|
2830
|
+
}
|
|
2831
|
+
changed = true;
|
|
2832
|
+
}
|
|
2833
|
+
if (changed)
|
|
2834
|
+
await this.#persist();
|
|
2835
|
+
return changed;
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Перенести закрепление с локального uuid на настоящий id Claude Code.
|
|
2839
|
+
*
|
|
2840
|
+
* Свежую сессию закрепляют до первого обмена — тогда её дискового id ещё не
|
|
2841
|
+
* существует, и в реестр попадает временный uuid. Как только id появился,
|
|
2842
|
+
* запись обязана переехать на него: под uuid сессию не найдёт ни список
|
|
2843
|
+
* (`#listSessions` ищет закрепление по дисковому id), ни следующий запуск
|
|
2844
|
+
* хоста — там этого uuid нет вовсе, и подметание стёрло бы закрепление
|
|
2845
|
+
* молча.
|
|
2846
|
+
*
|
|
2847
|
+
* Время закрепления переносится как есть: порядок в панели задан им, и
|
|
2848
|
+
* строка не должна прыгать в конец списка от того, что сессия наконец
|
|
2849
|
+
* дошла до `system/init`.
|
|
2850
|
+
*/
|
|
2851
|
+
async migrate(fromId, toId) {
|
|
2852
|
+
const record = this.#pinned.get(fromId);
|
|
2853
|
+
if (!record || fromId === toId)
|
|
2854
|
+
return false;
|
|
2855
|
+
this.#pinned.delete(fromId);
|
|
2856
|
+
if (!this.#pinned.has(toId))
|
|
2857
|
+
this.#pinned.set(toId, record);
|
|
2858
|
+
await this.#persist();
|
|
2859
|
+
return true;
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Забыть записи о сессиях, которых больше нет на диске.
|
|
2863
|
+
*
|
|
2864
|
+
* Зовётся оттуда же, откуда подметается архив, и делает то же самое: реестр
|
|
2865
|
+
* не должен копить ссылки на исчезнувшие транскрипты. `known` — все сессии
|
|
2866
|
+
* проекта, живые и с диска.
|
|
2867
|
+
*/
|
|
2868
|
+
async sweep(workspaceId, known) {
|
|
2869
|
+
let changed = false;
|
|
2870
|
+
for (const [id, record] of this.#pinned) {
|
|
2871
|
+
if (record.workspaceId !== workspaceId || known.has(id))
|
|
2872
|
+
continue;
|
|
2873
|
+
this.#pinned.delete(id);
|
|
2874
|
+
changed = true;
|
|
2875
|
+
}
|
|
2876
|
+
if (changed)
|
|
2877
|
+
await this.#persist();
|
|
2878
|
+
return changed;
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Забыть всё закреплённое в проекте — при его закрытии.
|
|
2882
|
+
*
|
|
2883
|
+
* Закрытый проект не показывается нигде, и закреплённая сессия из него
|
|
2884
|
+
* висела бы в панели строкой, по клику на которую открывать нечего.
|
|
2885
|
+
*/
|
|
2886
|
+
async forgetWorkspace(workspaceId) {
|
|
2887
|
+
let changed = false;
|
|
2888
|
+
for (const [id, record] of this.#pinned) {
|
|
2889
|
+
if (record.workspaceId !== workspaceId)
|
|
2890
|
+
continue;
|
|
2891
|
+
this.#pinned.delete(id);
|
|
2892
|
+
changed = true;
|
|
2893
|
+
}
|
|
2894
|
+
if (changed)
|
|
2895
|
+
await this.#persist();
|
|
2896
|
+
return changed;
|
|
2897
|
+
}
|
|
2898
|
+
async #persist() {
|
|
2899
|
+
const file = { pinned: Object.fromEntries(this.#pinned) };
|
|
2900
|
+
await mkdir4(path9.dirname(this.#storePath), { recursive: true });
|
|
2901
|
+
await writeFile4(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
function isRecord2(value) {
|
|
2905
|
+
return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// ../host/dist/app-settings.js
|
|
2909
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
2910
|
+
import { homedir as homedir3 } from "node:os";
|
|
2911
|
+
import path10 from "node:path";
|
|
2529
2912
|
var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
2530
2913
|
var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
|
|
2531
2914
|
var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
|
|
2532
2915
|
var FOLDER_ACCESS = ["all", "projects", "project"];
|
|
2916
|
+
var PERMISSION_MODES = [
|
|
2917
|
+
"default",
|
|
2918
|
+
"acceptEdits",
|
|
2919
|
+
"plan",
|
|
2920
|
+
"dontAsk",
|
|
2921
|
+
"bypassPermissions",
|
|
2922
|
+
"auto"
|
|
2923
|
+
];
|
|
2533
2924
|
var DEFAULT_APP_SETTINGS = {
|
|
2534
2925
|
autoArchiveAfterMs: DEFAULT_AUTO_ARCHIVE_AFTER_MS,
|
|
2535
2926
|
// Строгий режим по умолчанию: молча писать в чужой проект приложение начнёт
|
|
@@ -2537,17 +2928,26 @@ var DEFAULT_APP_SETTINGS = {
|
|
|
2537
2928
|
folderAccess: "project",
|
|
2538
2929
|
// Выключено по умолчанию: сохранение — явное действие, пока человек сам не
|
|
2539
2930
|
// попросил иначе.
|
|
2540
|
-
autoSaveAfterPauseMs: 0
|
|
2931
|
+
autoSaveAfterPauseMs: 0,
|
|
2932
|
+
// Модель за пользователя не выбираем: пока он не сказал иначе, сессия
|
|
2933
|
+
// поднимается ровно с той, с которой её поднял бы консольный Claude Code.
|
|
2934
|
+
startModel: null,
|
|
2935
|
+
// Тот же режим, с которым сессия стартовала до появления этой настройки.
|
|
2936
|
+
startPermissionMode: "default",
|
|
2937
|
+
// Терминал по умолчанию закрыт — его включают, а не выключают: приложение,
|
|
2938
|
+
// открытое в браузере, не обязано с первого запуска уметь запускать на этой
|
|
2939
|
+
// машине что угодно.
|
|
2940
|
+
terminalEnabled: false
|
|
2541
2941
|
};
|
|
2542
2942
|
var AppSettingsStore = class {
|
|
2543
2943
|
#settings = { ...DEFAULT_APP_SETTINGS };
|
|
2544
2944
|
#storePath;
|
|
2545
2945
|
constructor(dataDir) {
|
|
2546
|
-
this.#storePath =
|
|
2946
|
+
this.#storePath = path10.join(dataDir, "app-settings.json");
|
|
2547
2947
|
}
|
|
2548
2948
|
async load() {
|
|
2549
2949
|
try {
|
|
2550
|
-
const raw = await
|
|
2950
|
+
const raw = await readFile7(this.#storePath, "utf8");
|
|
2551
2951
|
this.#settings = sanitize(JSON.parse(raw), DEFAULT_APP_SETTINGS);
|
|
2552
2952
|
} catch {
|
|
2553
2953
|
this.#settings = { ...DEFAULT_APP_SETTINGS };
|
|
@@ -2567,8 +2967,8 @@ var AppSettingsStore = class {
|
|
|
2567
2967
|
async update(patch) {
|
|
2568
2968
|
const next = sanitize(patch, this.#settings);
|
|
2569
2969
|
this.#settings = next;
|
|
2570
|
-
await
|
|
2571
|
-
await
|
|
2970
|
+
await mkdir5(path10.dirname(this.#storePath), { recursive: true });
|
|
2971
|
+
await writeFile5(this.#storePath, JSON.stringify(next, null, 2), "utf8");
|
|
2572
2972
|
return this.current;
|
|
2573
2973
|
}
|
|
2574
2974
|
};
|
|
@@ -2581,18 +2981,32 @@ function sanitize(value, fallback) {
|
|
|
2581
2981
|
if (typeof after === "number" && Number.isFinite(after)) {
|
|
2582
2982
|
result.autoArchiveAfterMs = after <= 0 ? 0 : Math.max(Math.round(after), MIN_AUTO_ARCHIVE_AFTER_MS);
|
|
2583
2983
|
}
|
|
2584
|
-
const
|
|
2585
|
-
if (typeof
|
|
2586
|
-
result.folderAccess =
|
|
2984
|
+
const access3 = record.folderAccess;
|
|
2985
|
+
if (typeof access3 === "string" && FOLDER_ACCESS.includes(access3)) {
|
|
2986
|
+
result.folderAccess = access3;
|
|
2587
2987
|
}
|
|
2588
2988
|
const autoSave = record.autoSaveAfterPauseMs;
|
|
2589
2989
|
if (typeof autoSave === "number" && Number.isFinite(autoSave)) {
|
|
2590
2990
|
result.autoSaveAfterPauseMs = autoSave <= 0 ? 0 : Math.max(Math.round(autoSave), MIN_AUTO_SAVE_AFTER_PAUSE_MS);
|
|
2591
2991
|
}
|
|
2992
|
+
if ("startModel" in record) {
|
|
2993
|
+
const model = record.startModel;
|
|
2994
|
+
if (model === null)
|
|
2995
|
+
result.startModel = null;
|
|
2996
|
+
else if (typeof model === "string")
|
|
2997
|
+
result.startModel = model.trim() || null;
|
|
2998
|
+
}
|
|
2999
|
+
const mode = record.startPermissionMode;
|
|
3000
|
+
if (typeof mode === "string" && PERMISSION_MODES.includes(mode)) {
|
|
3001
|
+
result.startPermissionMode = mode;
|
|
3002
|
+
}
|
|
3003
|
+
if (typeof record.terminalEnabled === "boolean") {
|
|
3004
|
+
result.terminalEnabled = record.terminalEnabled;
|
|
3005
|
+
}
|
|
2592
3006
|
return result;
|
|
2593
3007
|
}
|
|
2594
3008
|
function claudeHome() {
|
|
2595
|
-
return
|
|
3009
|
+
return path10.join(homedir3(), ".claude");
|
|
2596
3010
|
}
|
|
2597
3011
|
function writableRoots(mode, cwd, workspacePaths) {
|
|
2598
3012
|
if (mode === "all")
|
|
@@ -2602,7 +3016,7 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
2602
3016
|
return [...roots];
|
|
2603
3017
|
for (const workspace of [cwd, ...workspacePaths]) {
|
|
2604
3018
|
roots.add(workspace);
|
|
2605
|
-
const parent =
|
|
3019
|
+
const parent = path10.dirname(workspace);
|
|
2606
3020
|
if (parent !== workspace)
|
|
2607
3021
|
roots.add(parent);
|
|
2608
3022
|
}
|
|
@@ -2610,9 +3024,9 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
2610
3024
|
}
|
|
2611
3025
|
|
|
2612
3026
|
// ../host/dist/plugins.js
|
|
2613
|
-
import { readFile as
|
|
3027
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2614
3028
|
import { homedir as homedir4 } from "node:os";
|
|
2615
|
-
import
|
|
3029
|
+
import path11 from "node:path";
|
|
2616
3030
|
var TIMEOUT_MS2 = 12e4;
|
|
2617
3031
|
var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
|
|
2618
3032
|
var PluginCli = class {
|
|
@@ -2711,10 +3125,10 @@ async function readCatalog(markets, installed) {
|
|
|
2711
3125
|
return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
2712
3126
|
}
|
|
2713
3127
|
async function readMarketplace(market, installed) {
|
|
2714
|
-
const root = market.installLocation ||
|
|
3128
|
+
const root = market.installLocation || path11.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
|
|
2715
3129
|
let manifest;
|
|
2716
3130
|
try {
|
|
2717
|
-
const text = await
|
|
3131
|
+
const text = await readFile8(path11.join(root, ".claude-plugin", "marketplace.json"), "utf8");
|
|
2718
3132
|
manifest = JSON.parse(text);
|
|
2719
3133
|
} catch {
|
|
2720
3134
|
return [];
|
|
@@ -2925,8 +3339,8 @@ function describe3(error) {
|
|
|
2925
3339
|
|
|
2926
3340
|
// ../host/dist/relay-link.js
|
|
2927
3341
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
2928
|
-
import { readFile as
|
|
2929
|
-
import
|
|
3342
|
+
import { readFile as readFile9, writeFile as writeFile6, rm as rm2, mkdir as mkdir6 } from "node:fs/promises";
|
|
3343
|
+
import path12 from "node:path";
|
|
2930
3344
|
import { hostname } from "node:os";
|
|
2931
3345
|
import WebSocket from "ws";
|
|
2932
3346
|
|
|
@@ -2948,13 +3362,26 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2948
3362
|
#clients = /* @__PURE__ */ new Set();
|
|
2949
3363
|
#error = null;
|
|
2950
3364
|
#closed = false;
|
|
3365
|
+
/**
|
|
3366
|
+
* Право удалённого терминала — то, что нам сказал relay, и ничего больше.
|
|
3367
|
+
*
|
|
3368
|
+
* На диск не пишется намеренно, в отличие от токена сопряжения: правда о
|
|
3369
|
+
* праве живёт на relay, а кеш на диске означал бы, что хост, поднятый без
|
|
3370
|
+
* связи, сам себе разрешает то, что мог отозвать другой человек час назад.
|
|
3371
|
+
* Поэтому после каждого запуска право закрыто, пока relay не пришлёт
|
|
3372
|
+
* `shell.state`, — и это ничего не стоит: удалённых вкладок до связи с relay
|
|
3373
|
+
* всё равно нет. Обрыв связи состояние не гасит по обратной причине: гасить
|
|
3374
|
+
* нечего (вкладок снова нет), а мигание «выключено» в настройках от каждого
|
|
3375
|
+
* зевка сети означало бы звать человека чинить то, что чинится само.
|
|
3376
|
+
*/
|
|
3377
|
+
#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
|
|
2951
3378
|
/** Куда сопрягаться, пока не сопряглись. */
|
|
2952
3379
|
#defaultUrl;
|
|
2953
3380
|
#file;
|
|
2954
3381
|
#log;
|
|
2955
3382
|
constructor(options2) {
|
|
2956
3383
|
super();
|
|
2957
|
-
this.#file =
|
|
3384
|
+
this.#file = path12.join(options2.dataDir, "relay.json");
|
|
2958
3385
|
this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
|
|
2959
3386
|
this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
|
|
2960
3387
|
}
|
|
@@ -2969,9 +3396,57 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2969
3396
|
name: this.#credentials?.name ?? null,
|
|
2970
3397
|
hostId: this.#credentials?.hostId ?? null,
|
|
2971
3398
|
clients: this.#clients.size,
|
|
3399
|
+
shell: this.#shell,
|
|
2972
3400
|
error: this.#error
|
|
2973
3401
|
};
|
|
2974
3402
|
}
|
|
3403
|
+
/** Право удалённого терминала — им же гейтится каждый кадр `term.*` от удалённой вкладки. */
|
|
3404
|
+
get shell() {
|
|
3405
|
+
return this.#shell;
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* Включить удалённый терминал прямо сейчас — вызывается только для запроса,
|
|
3409
|
+
* пришедшего с локального сокета.
|
|
3410
|
+
*
|
|
3411
|
+
* Решение «локально или удалённо» принимает `HostServer`, а не этот класс:
|
|
3412
|
+
* здесь уже нечем отличить одно от другого, а там это ровно то поле
|
|
3413
|
+
* `ClientChannel.local`, ради которого канал и заведён.
|
|
3414
|
+
*/
|
|
3415
|
+
async enableShell() {
|
|
3416
|
+
if (!this.#credentials)
|
|
3417
|
+
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");
|
|
3418
|
+
if (this.#socket?.readyState !== WebSocket.OPEN) {
|
|
3419
|
+
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");
|
|
3420
|
+
}
|
|
3421
|
+
this.#push({ t: "shell.enable" });
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Попросить relay выслать письмо, подтверждающее удалённый терминал.
|
|
3425
|
+
*
|
|
3426
|
+
* Само право этот вызов не даёт и дать не может: relay только отправляет
|
|
3427
|
+
* письмо, а включает доступ переход по ссылке из него. Поэтому здесь нет ни
|
|
3428
|
+
* ожидания ответа, ни оптимистичного `enabled = true` — состояние приедет
|
|
3429
|
+
* кадром `shell.state`, когда будет чему приезжать.
|
|
3430
|
+
*/
|
|
3431
|
+
async requestShell() {
|
|
3432
|
+
if (!this.#credentials)
|
|
3433
|
+
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");
|
|
3434
|
+
if (this.#socket?.readyState !== WebSocket.OPEN) {
|
|
3435
|
+
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");
|
|
3436
|
+
}
|
|
3437
|
+
this.#push({ t: "shell.request" });
|
|
3438
|
+
}
|
|
3439
|
+
/** Снять право немедленно. Тоже через relay: там оно и хранится. */
|
|
3440
|
+
async disableShell() {
|
|
3441
|
+
if (!this.#credentials)
|
|
3442
|
+
return;
|
|
3443
|
+
if (this.#socket?.readyState !== WebSocket.OPEN) {
|
|
3444
|
+
this.#shell = { ...this.#shell, enabled: false, pending: false };
|
|
3445
|
+
this.#changed();
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
this.#push({ t: "shell.disable" });
|
|
3449
|
+
}
|
|
2975
3450
|
#changed() {
|
|
2976
3451
|
this.emit("state");
|
|
2977
3452
|
}
|
|
@@ -2987,7 +3462,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2987
3462
|
}
|
|
2988
3463
|
async #read() {
|
|
2989
3464
|
try {
|
|
2990
|
-
const raw = await
|
|
3465
|
+
const raw = await readFile9(this.#file, "utf8");
|
|
2991
3466
|
const parsed = JSON.parse(raw);
|
|
2992
3467
|
if (typeof parsed !== "object" || parsed === null)
|
|
2993
3468
|
return null;
|
|
@@ -3000,8 +3475,8 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
3000
3475
|
}
|
|
3001
3476
|
}
|
|
3002
3477
|
async #write(credentials) {
|
|
3003
|
-
await
|
|
3004
|
-
await
|
|
3478
|
+
await mkdir6(path12.dirname(this.#file), { recursive: true });
|
|
3479
|
+
await writeFile6(this.#file, `${JSON.stringify(credentials, null, 2)}
|
|
3005
3480
|
`, {
|
|
3006
3481
|
encoding: "utf8",
|
|
3007
3482
|
mode: 384
|
|
@@ -3052,6 +3527,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
3052
3527
|
this.#credentials = null;
|
|
3053
3528
|
this.#error = null;
|
|
3054
3529
|
this.#clients.clear();
|
|
3530
|
+
this.#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
|
|
3055
3531
|
await rm2(this.#file, { force: true });
|
|
3056
3532
|
this.#changed();
|
|
3057
3533
|
this.#log("\u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435 \u0441\u043D\u044F\u0442\u043E");
|
|
@@ -3157,6 +3633,16 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
3157
3633
|
case "client.frame":
|
|
3158
3634
|
this.emit("frame", frame.clientId, frame.body);
|
|
3159
3635
|
return;
|
|
3636
|
+
case "shell.state":
|
|
3637
|
+
this.#shell = {
|
|
3638
|
+
enabled: frame.enabled,
|
|
3639
|
+
pending: frame.pending,
|
|
3640
|
+
email: frame.email,
|
|
3641
|
+
confirmedAt: frame.confirmedAt
|
|
3642
|
+
};
|
|
3643
|
+
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");
|
|
3644
|
+
this.#changed();
|
|
3645
|
+
return;
|
|
3160
3646
|
case "ping":
|
|
3161
3647
|
socket.send(JSON.stringify({ t: "pong", nonce: frame.nonce }));
|
|
3162
3648
|
return;
|
|
@@ -3197,11 +3683,196 @@ function describe4(error) {
|
|
|
3197
3683
|
return error instanceof Error ? error.message : String(error);
|
|
3198
3684
|
}
|
|
3199
3685
|
|
|
3686
|
+
// ../host/dist/terminal.js
|
|
3687
|
+
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
3688
|
+
import { access as access2, constants as constants2 } from "node:fs/promises";
|
|
3689
|
+
import path13 from "node:path";
|
|
3690
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3691
|
+
var SCROLLBACK_BYTES = 256 * 1024;
|
|
3692
|
+
var FLUSH_MS = 12;
|
|
3693
|
+
var MAX_TERMINALS = 24;
|
|
3694
|
+
var TerminalHub = class extends EventEmitter4 {
|
|
3695
|
+
#terminals = /* @__PURE__ */ new Map();
|
|
3696
|
+
#module = null;
|
|
3697
|
+
list(workspaceId) {
|
|
3698
|
+
const all = [...this.#terminals.values()].map((entry) => entry.info);
|
|
3699
|
+
return workspaceId ? all.filter((info) => info.workspaceId === workspaceId) : all;
|
|
3700
|
+
}
|
|
3701
|
+
has(id) {
|
|
3702
|
+
return this.#terminals.has(id);
|
|
3703
|
+
}
|
|
3704
|
+
/** Накопленный вывод — то, что увидит подключившийся позже. */
|
|
3705
|
+
buffer(id) {
|
|
3706
|
+
const entry = this.#terminals.get(id);
|
|
3707
|
+
if (!entry)
|
|
3708
|
+
return "";
|
|
3709
|
+
return entry.chunks.join("") + entry.pending;
|
|
3710
|
+
}
|
|
3711
|
+
async open(options2) {
|
|
3712
|
+
if (this.#terminals.size >= MAX_TERMINALS) {
|
|
3713
|
+
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`);
|
|
3714
|
+
}
|
|
3715
|
+
const pty = await this.#pty();
|
|
3716
|
+
const shell = await defaultShell();
|
|
3717
|
+
const cols = clampSize(options2.cols, 20, 500, 80);
|
|
3718
|
+
const rows = clampSize(options2.rows, 5, 200, 24);
|
|
3719
|
+
const child = pty.spawn(shell.file, shell.args, {
|
|
3720
|
+
name: "xterm-256color",
|
|
3721
|
+
cols,
|
|
3722
|
+
rows,
|
|
3723
|
+
cwd: options2.cwd,
|
|
3724
|
+
env: environment()
|
|
3725
|
+
});
|
|
3726
|
+
const info = {
|
|
3727
|
+
id: randomUUID2(),
|
|
3728
|
+
workspaceId: options2.workspaceId,
|
|
3729
|
+
title: shell.title,
|
|
3730
|
+
shell: shell.file,
|
|
3731
|
+
cwd: options2.cwd,
|
|
3732
|
+
cols,
|
|
3733
|
+
rows,
|
|
3734
|
+
exitCode: null
|
|
3735
|
+
};
|
|
3736
|
+
const entry = { info, pty: child, chunks: [], chunksLength: 0, pending: "", flush: null };
|
|
3737
|
+
this.#terminals.set(info.id, entry);
|
|
3738
|
+
child.onData((data) => this.#onData(entry, data));
|
|
3739
|
+
child.onExit(({ exitCode, signal }) => {
|
|
3740
|
+
this.#flush(entry);
|
|
3741
|
+
entry.info.exitCode = exitCode;
|
|
3742
|
+
this.emit("exit", info.id, exitCode, signal ?? null);
|
|
3743
|
+
});
|
|
3744
|
+
return info;
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Ввод человека — как есть.
|
|
3748
|
+
*
|
|
3749
|
+
* Ни разбора, ни экранирования, ни проверки: перед нами не «команда», а поток
|
|
3750
|
+
* нажатий, в котором Ctrl+C, Tab и вставка из буфера обмена неотличимы от
|
|
3751
|
+
* обычных букв и должны такими остаться.
|
|
3752
|
+
*/
|
|
3753
|
+
write(id, data) {
|
|
3754
|
+
const entry = this.#terminals.get(id);
|
|
3755
|
+
if (!entry)
|
|
3756
|
+
throw new Error("\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442");
|
|
3757
|
+
if (entry.info.exitCode !== null)
|
|
3758
|
+
return;
|
|
3759
|
+
entry.pty.write(data);
|
|
3760
|
+
}
|
|
3761
|
+
resize(id, cols, rows) {
|
|
3762
|
+
const entry = this.#terminals.get(id);
|
|
3763
|
+
if (!entry || entry.info.exitCode !== null)
|
|
3764
|
+
return;
|
|
3765
|
+
const next = { cols: clampSize(cols, 20, 500, 80), rows: clampSize(rows, 5, 200, 24) };
|
|
3766
|
+
if (next.cols === entry.info.cols && next.rows === entry.info.rows)
|
|
3767
|
+
return;
|
|
3768
|
+
entry.info.cols = next.cols;
|
|
3769
|
+
entry.info.rows = next.rows;
|
|
3770
|
+
try {
|
|
3771
|
+
entry.pty.resize(next.cols, next.rows);
|
|
3772
|
+
} catch {
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
close(id) {
|
|
3776
|
+
const entry = this.#terminals.get(id);
|
|
3777
|
+
if (!entry)
|
|
3778
|
+
return;
|
|
3779
|
+
this.#terminals.delete(id);
|
|
3780
|
+
if (entry.flush)
|
|
3781
|
+
clearTimeout(entry.flush);
|
|
3782
|
+
try {
|
|
3783
|
+
entry.pty.kill();
|
|
3784
|
+
} catch {
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
/** Погасить всё: незакрытые оболочки переживут хост и останутся висеть. */
|
|
3788
|
+
closeAll() {
|
|
3789
|
+
for (const id of [...this.#terminals.keys()])
|
|
3790
|
+
this.close(id);
|
|
3791
|
+
}
|
|
3792
|
+
#onData(entry, data) {
|
|
3793
|
+
entry.pending += data;
|
|
3794
|
+
if (entry.flush)
|
|
3795
|
+
return;
|
|
3796
|
+
entry.flush = setTimeout(() => this.#flush(entry), FLUSH_MS);
|
|
3797
|
+
entry.flush.unref?.();
|
|
3798
|
+
}
|
|
3799
|
+
#flush(entry) {
|
|
3800
|
+
if (entry.flush) {
|
|
3801
|
+
clearTimeout(entry.flush);
|
|
3802
|
+
entry.flush = null;
|
|
3803
|
+
}
|
|
3804
|
+
const data = entry.pending;
|
|
3805
|
+
if (!data)
|
|
3806
|
+
return;
|
|
3807
|
+
entry.pending = "";
|
|
3808
|
+
entry.chunks.push(data);
|
|
3809
|
+
entry.chunksLength += data.length;
|
|
3810
|
+
while (entry.chunksLength > SCROLLBACK_BYTES && entry.chunks.length > 1) {
|
|
3811
|
+
entry.chunksLength -= entry.chunks.shift().length;
|
|
3812
|
+
}
|
|
3813
|
+
this.emit("data", entry.info.id, data);
|
|
3814
|
+
}
|
|
3815
|
+
#pty() {
|
|
3816
|
+
this.#module ??= import("@lydell/node-pty").then((module) => module).catch((error) => {
|
|
3817
|
+
this.#module = null;
|
|
3818
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3819
|
+
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}`);
|
|
3820
|
+
});
|
|
3821
|
+
return this.#module;
|
|
3822
|
+
}
|
|
3823
|
+
};
|
|
3824
|
+
function clampSize(value, min, max, fallback) {
|
|
3825
|
+
if (!Number.isFinite(value))
|
|
3826
|
+
return fallback;
|
|
3827
|
+
return Math.min(max, Math.max(min, Math.round(value)));
|
|
3828
|
+
}
|
|
3829
|
+
function environment() {
|
|
3830
|
+
const env = {
|
|
3831
|
+
...process.env,
|
|
3832
|
+
TERM: "xterm-256color",
|
|
3833
|
+
COLORTERM: "truecolor"
|
|
3834
|
+
};
|
|
3835
|
+
for (const key of Object.keys(env))
|
|
3836
|
+
if (key.startsWith("="))
|
|
3837
|
+
delete env[key];
|
|
3838
|
+
return env;
|
|
3839
|
+
}
|
|
3840
|
+
async function defaultShell() {
|
|
3841
|
+
if (process.platform === "win32") {
|
|
3842
|
+
const pwsh = await findOnPath("pwsh.exe");
|
|
3843
|
+
if (pwsh)
|
|
3844
|
+
return { file: pwsh, args: [], title: "pwsh" };
|
|
3845
|
+
const system = process.env.SystemRoot ?? "C:\\Windows";
|
|
3846
|
+
const powershell = path13.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
|
|
3847
|
+
if (await exists2(powershell))
|
|
3848
|
+
return { file: powershell, args: [], title: "powershell" };
|
|
3849
|
+
return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
|
|
3850
|
+
}
|
|
3851
|
+
const shell = process.env.SHELL;
|
|
3852
|
+
if (shell && await exists2(shell))
|
|
3853
|
+
return { file: shell, args: [], title: path13.basename(shell) };
|
|
3854
|
+
if (await exists2("/bin/bash"))
|
|
3855
|
+
return { file: "/bin/bash", args: [], title: "bash" };
|
|
3856
|
+
return { file: "/bin/sh", args: [], title: "sh" };
|
|
3857
|
+
}
|
|
3858
|
+
async function findOnPath(name) {
|
|
3859
|
+
const dirs = (process.env.PATH ?? "").split(path13.delimiter).filter(Boolean);
|
|
3860
|
+
for (const dir of dirs) {
|
|
3861
|
+
const candidate = path13.join(dir, name);
|
|
3862
|
+
if (await exists2(candidate))
|
|
3863
|
+
return candidate;
|
|
3864
|
+
}
|
|
3865
|
+
return null;
|
|
3866
|
+
}
|
|
3867
|
+
function exists2(file) {
|
|
3868
|
+
return access2(file, constants2.X_OK).then(() => true, () => false);
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3200
3871
|
// ../host/dist/mcp-config.js
|
|
3201
|
-
import { readFile as
|
|
3202
|
-
import
|
|
3872
|
+
import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7 } from "node:fs/promises";
|
|
3873
|
+
import path14 from "node:path";
|
|
3203
3874
|
var MCP_FILE = ".mcp.json";
|
|
3204
|
-
var LOCAL_SETTINGS2 =
|
|
3875
|
+
var LOCAL_SETTINGS2 = path14.join(".claude", "settings.local.json");
|
|
3205
3876
|
var queues = /* @__PURE__ */ new Map();
|
|
3206
3877
|
function serialize(cwd, work) {
|
|
3207
3878
|
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
@@ -3210,7 +3881,7 @@ function serialize(cwd, work) {
|
|
|
3210
3881
|
return next;
|
|
3211
3882
|
}
|
|
3212
3883
|
async function readProjectServers(cwd) {
|
|
3213
|
-
const file = await readJson(
|
|
3884
|
+
const file = await readJson(path14.join(cwd, MCP_FILE));
|
|
3214
3885
|
const servers = file?.mcpServers;
|
|
3215
3886
|
if (!servers || typeof servers !== "object")
|
|
3216
3887
|
return {};
|
|
@@ -3218,23 +3889,23 @@ async function readProjectServers(cwd) {
|
|
|
3218
3889
|
}
|
|
3219
3890
|
function addProjectServer(cwd, name, config) {
|
|
3220
3891
|
return serialize(cwd, async () => {
|
|
3221
|
-
const file = await readJson(
|
|
3892
|
+
const file = await readJson(path14.join(cwd, MCP_FILE)) ?? {};
|
|
3222
3893
|
const servers = file.mcpServers ?? {};
|
|
3223
3894
|
servers[name] = config;
|
|
3224
3895
|
file.mcpServers = servers;
|
|
3225
|
-
await writeJson(
|
|
3896
|
+
await writeJson(path14.join(cwd, MCP_FILE), file);
|
|
3226
3897
|
await approve(cwd, name);
|
|
3227
3898
|
});
|
|
3228
3899
|
}
|
|
3229
3900
|
function removeProjectServer(cwd, name) {
|
|
3230
3901
|
return serialize(cwd, async () => {
|
|
3231
|
-
const file = await readJson(
|
|
3902
|
+
const file = await readJson(path14.join(cwd, MCP_FILE));
|
|
3232
3903
|
const servers = file?.mcpServers;
|
|
3233
3904
|
if (!servers || !(name in servers))
|
|
3234
3905
|
return false;
|
|
3235
3906
|
delete servers[name];
|
|
3236
|
-
await writeJson(
|
|
3237
|
-
const settingsPath =
|
|
3907
|
+
await writeJson(path14.join(cwd, MCP_FILE), file);
|
|
3908
|
+
const settingsPath = path14.join(cwd, LOCAL_SETTINGS2);
|
|
3238
3909
|
const settings = await readJson(settingsPath);
|
|
3239
3910
|
const enabled = settings?.enabledMcpjsonServers;
|
|
3240
3911
|
if (settings && Array.isArray(enabled) && enabled.includes(name)) {
|
|
@@ -3245,7 +3916,7 @@ function removeProjectServer(cwd, name) {
|
|
|
3245
3916
|
});
|
|
3246
3917
|
}
|
|
3247
3918
|
async function approve(cwd, name) {
|
|
3248
|
-
const file =
|
|
3919
|
+
const file = path14.join(cwd, LOCAL_SETTINGS2);
|
|
3249
3920
|
const settings = await readJson(file) ?? {};
|
|
3250
3921
|
const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
3251
3922
|
if (!enabled.includes(name))
|
|
@@ -3284,31 +3955,34 @@ function describeConfig(config) {
|
|
|
3284
3955
|
}
|
|
3285
3956
|
async function readJson(file) {
|
|
3286
3957
|
try {
|
|
3287
|
-
const raw = await
|
|
3958
|
+
const raw = await readFile10(file, "utf8");
|
|
3288
3959
|
const parsed = JSON.parse(raw);
|
|
3289
3960
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
3290
3961
|
} catch (error) {
|
|
3291
3962
|
if (error.code === "ENOENT")
|
|
3292
3963
|
return null;
|
|
3293
3964
|
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 ${
|
|
3965
|
+
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
3966
|
}
|
|
3296
3967
|
throw error;
|
|
3297
3968
|
}
|
|
3298
3969
|
}
|
|
3299
3970
|
async function writeJson(file, data) {
|
|
3300
|
-
await
|
|
3301
|
-
await
|
|
3971
|
+
await mkdir7(path14.dirname(file), { recursive: true });
|
|
3972
|
+
await writeFile7(file, `${JSON.stringify(data, null, 2)}
|
|
3302
3973
|
`, "utf8");
|
|
3303
3974
|
}
|
|
3304
3975
|
|
|
3305
3976
|
// ../host/dist/server.js
|
|
3306
3977
|
var SIDE_TITLE = "\xB7 \u0432\u043E\u043F\u0440\u043E\u0441 \u043D\u0430 \u043F\u043E\u043B\u044F\u0445";
|
|
3307
3978
|
var FINDER_TITLE = "\xB7 \u043F\u043E\u0438\u0441\u043A \u043F\u0440\u043E\u0435\u043A\u0442\u0430";
|
|
3979
|
+
var HISTORY_PAGE = 250;
|
|
3980
|
+
var HISTORY_PAGE_MAX = 500;
|
|
3308
3981
|
var HostServer = class {
|
|
3309
3982
|
#options;
|
|
3310
3983
|
#workspaces;
|
|
3311
3984
|
#archive;
|
|
3985
|
+
#pins;
|
|
3312
3986
|
#settings;
|
|
3313
3987
|
#finder;
|
|
3314
3988
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -3326,18 +4000,21 @@ var HostServer = class {
|
|
|
3326
4000
|
#clients = /* @__PURE__ */ new Set();
|
|
3327
4001
|
/** Удалённые вкладки по их id в relay — чтобы адресовать ответ именно той. */
|
|
3328
4002
|
#remotes = /* @__PURE__ */ new Map();
|
|
3329
|
-
#hostId =
|
|
4003
|
+
#hostId = randomUUID3();
|
|
3330
4004
|
#plugins = new PluginCli();
|
|
3331
4005
|
#auth = new AuthCli();
|
|
3332
4006
|
#relay;
|
|
4007
|
+
#terminals = new TerminalHub();
|
|
3333
4008
|
constructor(options2) {
|
|
3334
4009
|
this.#options = options2;
|
|
3335
4010
|
this.#workspaces = new WorkspaceRegistry(options2.dataDir);
|
|
3336
4011
|
this.#archive = new SessionArchive(options2.dataDir);
|
|
4012
|
+
this.#pins = new SessionPins(options2.dataDir);
|
|
3337
4013
|
this.#settings = new AppSettingsStore(options2.dataDir);
|
|
3338
4014
|
this.#finder = new ProjectFinder(this.#workspaces);
|
|
3339
4015
|
this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
|
|
3340
4016
|
this.#wireRelay();
|
|
4017
|
+
this.#wireTerminals();
|
|
3341
4018
|
this.#auth.on("login", (state) => {
|
|
3342
4019
|
this.#broadcast({ type: "auth.login", state });
|
|
3343
4020
|
});
|
|
@@ -3348,6 +4025,7 @@ var HostServer = class {
|
|
|
3348
4025
|
async start() {
|
|
3349
4026
|
await this.#workspaces.load();
|
|
3350
4027
|
await this.#archive.load();
|
|
4028
|
+
await this.#pins.load();
|
|
3351
4029
|
await this.#settings.load();
|
|
3352
4030
|
await warmClaudeLauncher();
|
|
3353
4031
|
void this.#finder.warm();
|
|
@@ -3373,6 +4051,7 @@ var HostServer = class {
|
|
|
3373
4051
|
}
|
|
3374
4052
|
async shutdown() {
|
|
3375
4053
|
this.#auth.cancel();
|
|
4054
|
+
this.#terminals.closeAll();
|
|
3376
4055
|
await this.#relay.close();
|
|
3377
4056
|
await Promise.all([...this.#sessions.values()].map((s) => s.close("host shutdown")));
|
|
3378
4057
|
this.#sessions.clear();
|
|
@@ -3436,6 +4115,36 @@ var HostServer = class {
|
|
|
3436
4115
|
this.#broadcast({ type: "relay.state", state: this.#relay.state });
|
|
3437
4116
|
});
|
|
3438
4117
|
}
|
|
4118
|
+
// -------------------------------------------------------------------------
|
|
4119
|
+
// Терминал
|
|
4120
|
+
// -------------------------------------------------------------------------
|
|
4121
|
+
#wireTerminals() {
|
|
4122
|
+
this.#terminals.on("data", (id, data) => {
|
|
4123
|
+
this.#broadcastTerminal({ type: "term.data", id, data });
|
|
4124
|
+
});
|
|
4125
|
+
this.#terminals.on("exit", (id, exitCode, signal) => {
|
|
4126
|
+
this.#broadcastTerminal({ type: "term.exit", id, exitCode, signal });
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Вывод терминала — всем локальным вкладкам, удалённым только по праву.
|
|
4131
|
+
*
|
|
4132
|
+
* Отдельно от `#broadcast` именно поэтому: обычный broadcast уходит и в
|
|
4133
|
+
* relay, а вывод оболочки — то самое, что удалённой стороне видеть не
|
|
4134
|
+
* положено, пока доступ не подтверждён письмом. Ошибиться здесь дороже, чем
|
|
4135
|
+
* в проверке входящего кадра: вход можно не заметить и отклонить, а вывод,
|
|
4136
|
+
* ушедший наружу, уже ушёл.
|
|
4137
|
+
*/
|
|
4138
|
+
#broadcastTerminal(frame) {
|
|
4139
|
+
if (this.#relay.shell.enabled) {
|
|
4140
|
+
this.#broadcast(frame);
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
for (const client of this.#clients) {
|
|
4144
|
+
if (client.local)
|
|
4145
|
+
client.send(frame);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
3439
4148
|
#adoptRemote(clientId) {
|
|
3440
4149
|
const client = {
|
|
3441
4150
|
local: false,
|
|
@@ -3509,10 +4218,17 @@ var HostServer = class {
|
|
|
3509
4218
|
continue;
|
|
3510
4219
|
await this.#dropSession(session.id, "workspace closed");
|
|
3511
4220
|
}
|
|
4221
|
+
let unpinned = false;
|
|
4222
|
+
for (const id of gone) {
|
|
4223
|
+
if (await this.#pins.forgetWorkspace(id))
|
|
4224
|
+
unpinned = true;
|
|
4225
|
+
}
|
|
3512
4226
|
this.#broadcast({
|
|
3513
4227
|
type: "workspace.list",
|
|
3514
4228
|
workspaces: this.#workspaces.list()
|
|
3515
4229
|
});
|
|
4230
|
+
if (unpinned)
|
|
4231
|
+
await this.#sendPinned();
|
|
3516
4232
|
return;
|
|
3517
4233
|
}
|
|
3518
4234
|
case "workspace.find": {
|
|
@@ -3575,6 +4291,27 @@ var HostServer = class {
|
|
|
3575
4291
|
await this.#archive.unarchive(frame.sessionIds);
|
|
3576
4292
|
await this.#sendSessionList(frame.workspaceId);
|
|
3577
4293
|
return;
|
|
4294
|
+
case "session.pin": {
|
|
4295
|
+
const ids = new Set(frame.sessionIds);
|
|
4296
|
+
for (const session of [...this.#sessions.values()]) {
|
|
4297
|
+
const diskId = session.claudeSessionId;
|
|
4298
|
+
if (!diskId)
|
|
4299
|
+
continue;
|
|
4300
|
+
if (ids.has(session.id))
|
|
4301
|
+
ids.add(diskId);
|
|
4302
|
+
if (ids.has(diskId))
|
|
4303
|
+
ids.add(session.id);
|
|
4304
|
+
}
|
|
4305
|
+
await this.#pins.set(frame.workspaceId, [...ids], frame.pinned);
|
|
4306
|
+
const hadPins = this.#pins.workspaceIds().includes(frame.workspaceId);
|
|
4307
|
+
await this.#sendSessionList(frame.workspaceId);
|
|
4308
|
+
if (!hadPins)
|
|
4309
|
+
await this.#sendPinned();
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
case "session.pins":
|
|
4313
|
+
await this.#sendPinned();
|
|
4314
|
+
return;
|
|
3578
4315
|
case "session.rename":
|
|
3579
4316
|
await this.#renameSession(frame.sessionId, frame.title, frame.workspaceId);
|
|
3580
4317
|
return;
|
|
@@ -3601,6 +4338,21 @@ var HostServer = class {
|
|
|
3601
4338
|
}
|
|
3602
4339
|
return;
|
|
3603
4340
|
}
|
|
4341
|
+
case "session.history": {
|
|
4342
|
+
const session = this.#requireSession(frame.sessionId);
|
|
4343
|
+
const all = await session.history();
|
|
4344
|
+
const end = typeof frame.before === "number" ? Math.max(0, Math.min(all.length, frame.before - 1)) : all.length;
|
|
4345
|
+
const limit = Math.min(Math.max(frame.limit ?? HISTORY_PAGE, 1), HISTORY_PAGE_MAX);
|
|
4346
|
+
const start = Math.max(0, end - limit);
|
|
4347
|
+
this.#send(socket, {
|
|
4348
|
+
type: "session.history",
|
|
4349
|
+
sessionId: session.id,
|
|
4350
|
+
entries: all.slice(start, end),
|
|
4351
|
+
from: start + 1,
|
|
4352
|
+
total: all.length
|
|
4353
|
+
});
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
3604
4356
|
case "usage.get": {
|
|
3605
4357
|
const session = this.#requireSession(frame.sessionId);
|
|
3606
4358
|
if (session.usage) {
|
|
@@ -3822,6 +4574,54 @@ var HostServer = class {
|
|
|
3822
4574
|
}
|
|
3823
4575
|
return;
|
|
3824
4576
|
}
|
|
4577
|
+
// Терминал. Каждая ручка начинается с одной и той же проверки права: их
|
|
4578
|
+
// пять, и «забыл в одной» здесь означает не потерянную кнопку, а чужую
|
|
4579
|
+
// оболочку в руках у того, кому её не давали.
|
|
4580
|
+
case "term.list":
|
|
4581
|
+
this.#requireShellAccess(socket);
|
|
4582
|
+
this.#send(socket, {
|
|
4583
|
+
type: "term.list",
|
|
4584
|
+
workspaceId: frame.workspaceId,
|
|
4585
|
+
terminals: this.#terminals.list(frame.workspaceId)
|
|
4586
|
+
});
|
|
4587
|
+
return;
|
|
4588
|
+
case "term.open": {
|
|
4589
|
+
this.#requireShellAccess(socket);
|
|
4590
|
+
const workspace = this.#workspaces.find(frame.workspaceId);
|
|
4591
|
+
if (!workspace)
|
|
4592
|
+
throw new Error(`\u0412\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${frame.workspaceId}`);
|
|
4593
|
+
const terminal = await this.#terminals.open({
|
|
4594
|
+
workspaceId: workspace.id,
|
|
4595
|
+
cwd: workspace.path,
|
|
4596
|
+
cols: frame.cols,
|
|
4597
|
+
rows: frame.rows
|
|
4598
|
+
});
|
|
4599
|
+
this.#broadcastTerminal({ type: "term.opened", terminal });
|
|
4600
|
+
return;
|
|
4601
|
+
}
|
|
4602
|
+
case "term.attach":
|
|
4603
|
+
this.#requireShellAccess(socket);
|
|
4604
|
+
if (!this.#terminals.has(frame.id))
|
|
4605
|
+
throw new Error("\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442");
|
|
4606
|
+
this.#send(socket, {
|
|
4607
|
+
type: "term.buffer",
|
|
4608
|
+
id: frame.id,
|
|
4609
|
+
data: this.#terminals.buffer(frame.id)
|
|
4610
|
+
});
|
|
4611
|
+
return;
|
|
4612
|
+
case "term.input":
|
|
4613
|
+
this.#requireShellAccess(socket);
|
|
4614
|
+
this.#terminals.write(frame.id, frame.data);
|
|
4615
|
+
return;
|
|
4616
|
+
case "term.resize":
|
|
4617
|
+
this.#requireShellAccess(socket);
|
|
4618
|
+
this.#terminals.resize(frame.id, frame.cols, frame.rows);
|
|
4619
|
+
return;
|
|
4620
|
+
case "term.close":
|
|
4621
|
+
this.#requireShellAccess(socket);
|
|
4622
|
+
this.#terminals.close(frame.id);
|
|
4623
|
+
this.#broadcastTerminal({ type: "term.closed", id: frame.id });
|
|
4624
|
+
return;
|
|
3825
4625
|
case "settings.get":
|
|
3826
4626
|
this.#send(socket, { type: "settings.state", settings: this.#settings.current });
|
|
3827
4627
|
return;
|
|
@@ -3834,6 +4634,12 @@ var HostServer = class {
|
|
|
3834
4634
|
await this.#sendSessionList(workspace.id);
|
|
3835
4635
|
}
|
|
3836
4636
|
}
|
|
4637
|
+
if (before.terminalEnabled && !settings.terminalEnabled) {
|
|
4638
|
+
for (const terminal of this.#terminals.list()) {
|
|
4639
|
+
this.#terminals.close(terminal.id);
|
|
4640
|
+
this.#broadcastTerminal({ type: "term.closed", id: terminal.id });
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
3837
4643
|
return;
|
|
3838
4644
|
}
|
|
3839
4645
|
case "auth.status":
|
|
@@ -3863,8 +4669,41 @@ var HostServer = class {
|
|
|
3863
4669
|
this.#requireLocal(socket, "\u041E\u0442\u0432\u044F\u0437\u0430\u0442\u044C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440");
|
|
3864
4670
|
await this.#relay.unpair();
|
|
3865
4671
|
return;
|
|
4672
|
+
case "relay.shell.enable":
|
|
4673
|
+
if (!this.#settings.current.terminalEnabled) {
|
|
4674
|
+
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.");
|
|
4675
|
+
}
|
|
4676
|
+
if (socket.local)
|
|
4677
|
+
await this.#relay.enableShell();
|
|
4678
|
+
else
|
|
4679
|
+
await this.#relay.requestShell();
|
|
4680
|
+
return;
|
|
4681
|
+
case "relay.shell.disable":
|
|
4682
|
+
await this.#relay.disableShell();
|
|
4683
|
+
return;
|
|
3866
4684
|
}
|
|
3867
4685
|
}
|
|
4686
|
+
/**
|
|
4687
|
+
* Терминал — по двум условиям сразу: включён ли он вообще и, для удалённой
|
|
4688
|
+
* вкладки, подтверждено ли право.
|
|
4689
|
+
*
|
|
4690
|
+
* Первое — настройка самой машины (`AppSettings.terminalEnabled`), выключена
|
|
4691
|
+
* по умолчанию: пока её не подняли, терминала нет ни у кого. Второе, поверх
|
|
4692
|
+
* первого: локальному клиенту не отказываем никогда — он и так за этой
|
|
4693
|
+
* машиной, где терминал открывается и без нас. Удалённому — по состоянию,
|
|
4694
|
+
* которое ведёт
|
|
4695
|
+
* relay: право включают либо на самой машине (сразу, физический доступ и
|
|
4696
|
+
* есть подтверждение), либо из удалённой вкладки, и тогда его включает не
|
|
4697
|
+
* нажатие кнопки, а переход по ссылке из письма на почту аккаунта.
|
|
4698
|
+
*/
|
|
4699
|
+
#requireShellAccess(client) {
|
|
4700
|
+
if (!this.#settings.current.terminalEnabled) {
|
|
4701
|
+
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.");
|
|
4702
|
+
}
|
|
4703
|
+
if (client.local || this.#relay.shell.enabled)
|
|
4704
|
+
return;
|
|
4705
|
+
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.");
|
|
4706
|
+
}
|
|
3868
4707
|
/**
|
|
3869
4708
|
* Настройки удалённого доступа меняются только с самой машины.
|
|
3870
4709
|
*
|
|
@@ -3997,6 +4836,32 @@ var HostServer = class {
|
|
|
3997
4836
|
async #sendSessionList(workspaceId) {
|
|
3998
4837
|
const { sessions, archived } = await this.#listSessions(workspaceId);
|
|
3999
4838
|
this.#broadcast({ type: "session.list", workspaceId, sessions, archived });
|
|
4839
|
+
if (this.#pins.workspaceIds().includes(workspaceId))
|
|
4840
|
+
await this.#sendPinned();
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Разослать закреплённые сессии — по всем проектам сразу.
|
|
4844
|
+
*
|
|
4845
|
+
* Собирается из тех же списков, что и панель проекта (`#listSessions`), а не
|
|
4846
|
+
* из одного реестра закреплений: в панели закреплённых у строки должны быть
|
|
4847
|
+
* имя, состояние и время последнего события, а реестр знает только id. Обход
|
|
4848
|
+
* идёт лишь по проектам, где вообще что-то закреплено, — их обычно единицы, и
|
|
4849
|
+
* читать диск по всем открытым проектам ради этого незачем.
|
|
4850
|
+
*
|
|
4851
|
+
* Архивные сюда тоже попадают: закреплённое в архив само не уезжает, но
|
|
4852
|
+
* убрать туда руками уже закреплённую сессию человеку никто не мешает, и
|
|
4853
|
+
* терять её из панели после этого было бы странно.
|
|
4854
|
+
*/
|
|
4855
|
+
async #sendPinned() {
|
|
4856
|
+
const sessions = [];
|
|
4857
|
+
for (const workspaceId of this.#pins.workspaceIds()) {
|
|
4858
|
+
if (!this.#workspaces.find(workspaceId))
|
|
4859
|
+
continue;
|
|
4860
|
+
const { sessions: live, archived } = await this.#listSessions(workspaceId);
|
|
4861
|
+
sessions.push(...[...live, ...archived].filter((s) => s.pinned));
|
|
4862
|
+
}
|
|
4863
|
+
sessions.sort((a, b) => (a.pinnedAt ?? 0) - (b.pinnedAt ?? 0));
|
|
4864
|
+
this.#broadcast({ type: "session.pinned", sessions });
|
|
4000
4865
|
}
|
|
4001
4866
|
async #startSession(socket, frame) {
|
|
4002
4867
|
const workspace = this.#workspaces.find(frame.workspaceId);
|
|
@@ -4006,13 +4871,14 @@ var HostServer = class {
|
|
|
4006
4871
|
const resumedTitle = resumed ? diskTitle(resumed) : null;
|
|
4007
4872
|
if (frame.resume)
|
|
4008
4873
|
await this.#archive.unarchive([frame.resume]);
|
|
4874
|
+
const startDefaults = this.#settings.current;
|
|
4009
4875
|
const session = new ClaudeSession({
|
|
4010
4876
|
workspaceId: workspace.id,
|
|
4011
4877
|
cwd: workspace.path,
|
|
4012
4878
|
resume: frame.resume,
|
|
4013
4879
|
fork: frame.fork,
|
|
4014
|
-
permissionMode: frame.permissionMode,
|
|
4015
|
-
model: frame.model,
|
|
4880
|
+
permissionMode: frame.permissionMode ?? startDefaults.startPermissionMode,
|
|
4881
|
+
model: frame.model ?? startDefaults.startModel ?? void 0,
|
|
4016
4882
|
...resumedTitle ? { title: resumedTitle } : {},
|
|
4017
4883
|
// Имя проекта вместо «Без названия»: свежую сессию в списке уже надо
|
|
4018
4884
|
// как-то называть, а сказать о ней пока нечего, кроме того, где она.
|
|
@@ -4036,6 +4902,9 @@ var HostServer = class {
|
|
|
4036
4902
|
});
|
|
4037
4903
|
session.on("summary", (summary) => {
|
|
4038
4904
|
this.#broadcast({ type: "session.summary", session: summary });
|
|
4905
|
+
if (!summary.claudeSessionId || !this.#pins.has(summary.id))
|
|
4906
|
+
return;
|
|
4907
|
+
void this.#pins.migrate(summary.id, summary.claudeSessionId).then((moved) => moved ? this.#sendPinned() : void 0).catch(() => void 0);
|
|
4039
4908
|
});
|
|
4040
4909
|
session.on("capabilities", () => {
|
|
4041
4910
|
if (!session.capabilities)
|
|
@@ -4298,6 +5167,9 @@ var HostServer = class {
|
|
|
4298
5167
|
parentSessionId: null,
|
|
4299
5168
|
title: diskTitle(info),
|
|
4300
5169
|
state: "closed",
|
|
5170
|
+
// Хода в этой жизни у неё не было: сессия не поднята, и чем кончился
|
|
5171
|
+
// последний ход прошлой, знает только её JSONL.
|
|
5172
|
+
lastTurn: null,
|
|
4301
5173
|
model: null,
|
|
4302
5174
|
permissionMode: "default",
|
|
4303
5175
|
// Сессия с диска не поднята: чем она была настроена, знает только её
|
|
@@ -4311,10 +5183,14 @@ var HostServer = class {
|
|
|
4311
5183
|
lastSeq: 0,
|
|
4312
5184
|
totalCostUsd: 0
|
|
4313
5185
|
}));
|
|
4314
|
-
|
|
5186
|
+
const known = /* @__PURE__ */ new Set([...liveIds, ...historical.map((s) => s.id)]);
|
|
5187
|
+
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);
|
|
5188
|
+
await this.#pins.sweep(workspaceId, known);
|
|
4315
5189
|
const sessions = [];
|
|
4316
5190
|
const archived = [];
|
|
4317
|
-
for (const
|
|
5191
|
+
for (const base of [...live, ...historical].sort((a, b) => b.updatedAt - a.updatedAt)) {
|
|
5192
|
+
const pin = this.#pins.find(base.claudeSessionId ?? base.id);
|
|
5193
|
+
const summary = pin ? { ...base, pinned: true, pinnedAt: pin.at } : base;
|
|
4318
5194
|
const record = this.#archive.find(summary.claudeSessionId ?? summary.id);
|
|
4319
5195
|
if (!record) {
|
|
4320
5196
|
sessions.push(summary);
|
|
@@ -4352,8 +5228,8 @@ var HostServer = class {
|
|
|
4352
5228
|
return;
|
|
4353
5229
|
}
|
|
4354
5230
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
4355
|
-
const requested =
|
|
4356
|
-
const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested :
|
|
5231
|
+
const requested = path15.join(webRoot, path15.normalize(url2.pathname));
|
|
5232
|
+
const target = requested.startsWith(webRoot) && existsSync3(requested) && statSync3(requested).isFile() ? requested : path15.join(webRoot, "index.html");
|
|
4357
5233
|
if (!existsSync3(target)) {
|
|
4358
5234
|
res.writeHead(404).end("not found");
|
|
4359
5235
|
return;
|
|
@@ -4379,12 +5255,12 @@ function contentType(file) {
|
|
|
4379
5255
|
".svg": "image/svg+xml",
|
|
4380
5256
|
".woff2": "font/woff2"
|
|
4381
5257
|
};
|
|
4382
|
-
return types[
|
|
5258
|
+
return types[path15.extname(file)] ?? "application/octet-stream";
|
|
4383
5259
|
}
|
|
4384
5260
|
|
|
4385
5261
|
// dist/ccv.js
|
|
4386
|
-
var HERE =
|
|
4387
|
-
var PACKAGE_ROOT =
|
|
5262
|
+
var HERE = path16.dirname(fileURLToPath(import.meta.url));
|
|
5263
|
+
var PACKAGE_ROOT = path16.resolve(HERE, "..");
|
|
4388
5264
|
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
5265
|
|
|
4390
5266
|
\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
|
|
@@ -4451,7 +5327,7 @@ var server = new HostServer({
|
|
|
4451
5327
|
dataDir: defaults.dataDir,
|
|
4452
5328
|
// UI лежит в пакете рядом с бандлом — это и есть разница между установленной
|
|
4453
5329
|
// утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
|
|
4454
|
-
webRoot: process.env.CCV_WEB_ROOT ??
|
|
5330
|
+
webRoot: process.env.CCV_WEB_ROOT ?? path16.join(PACKAGE_ROOT, "web"),
|
|
4455
5331
|
version,
|
|
4456
5332
|
relayUrl: defaults.relayUrl
|
|
4457
5333
|
});
|
|
@@ -4480,7 +5356,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
4480
5356
|
}
|
|
4481
5357
|
async function packageVersion() {
|
|
4482
5358
|
try {
|
|
4483
|
-
const text = await
|
|
5359
|
+
const text = await readFile11(path16.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
4484
5360
|
return JSON.parse(text).version ?? "0.0.0";
|
|
4485
5361
|
} catch {
|
|
4486
5362
|
return "0.0.0";
|