@i14ks/ccv 0.1.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/LICENSE +173 -173
- package/README.md +56 -56
- package/THIRD-PARTY-NOTICES.md +91 -5
- package/bundle/ccv.mjs +1051 -68
- package/package.json +50 -49
- 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-C-HuAiCn.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-DJN3PaCw.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-D0qAiokq.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 +40 -30
- package/web/assets/index-4x6fBcbV.js +0 -1
- package/web/assets/index-B4RnRe_M.js +0 -133
- package/web/assets/index-BBL1LP-l.js +0 -1
- package/web/assets/index-BQbBSCN3.js +0 -1
- package/web/assets/index-BVHAVdqc.js +0 -3
- package/web/assets/index-CG3BWhrU.js +0 -7
- package/web/assets/index-CPBnfhi2.js +0 -1
- package/web/assets/index-CYyCCyf_.js +0 -1
- package/web/assets/index-CtQ2lLVm.js +0 -1
- package/web/assets/index-D8swzi0v.js +0 -13
- package/web/assets/index-DSNRlHPG.js +0 -1
- package/web/assets/index-Dw1tTDZW.js +0 -1
- package/web/assets/index-e0Ev4udb.js +0 -1
- package/web/assets/index-uKB-GMLe.css +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.";
|
|
@@ -646,6 +824,80 @@ function writeTarget(input) {
|
|
|
646
824
|
const target = record.file_path ?? record.notebook_path;
|
|
647
825
|
return typeof target === "string" && target ? target : null;
|
|
648
826
|
}
|
|
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
|
+
}
|
|
851
|
+
function parseQuestions(input) {
|
|
852
|
+
if (typeof input !== "object" || input === null)
|
|
853
|
+
return null;
|
|
854
|
+
const raw = input.questions;
|
|
855
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
856
|
+
return null;
|
|
857
|
+
const questions = [];
|
|
858
|
+
for (const item of raw) {
|
|
859
|
+
if (typeof item !== "object" || item === null)
|
|
860
|
+
return null;
|
|
861
|
+
const { question, header, options: options2, multiSelect } = item;
|
|
862
|
+
if (typeof question !== "string" || !question)
|
|
863
|
+
return null;
|
|
864
|
+
if (!Array.isArray(options2) || options2.length === 0)
|
|
865
|
+
return null;
|
|
866
|
+
const parsed = [];
|
|
867
|
+
for (const option of options2) {
|
|
868
|
+
if (typeof option !== "object" || option === null)
|
|
869
|
+
return null;
|
|
870
|
+
const { label, description, preview } = option;
|
|
871
|
+
if (typeof label !== "string" || !label)
|
|
872
|
+
return null;
|
|
873
|
+
parsed.push({
|
|
874
|
+
label,
|
|
875
|
+
description: typeof description === "string" ? description : "",
|
|
876
|
+
...typeof preview === "string" && preview ? { preview } : {}
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
questions.push({
|
|
880
|
+
question,
|
|
881
|
+
header: typeof header === "string" ? header : "",
|
|
882
|
+
options: parsed,
|
|
883
|
+
multiSelect: multiSelect === true
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return questions;
|
|
887
|
+
}
|
|
888
|
+
function previewAnnotations(questions, answers) {
|
|
889
|
+
const annotations = {};
|
|
890
|
+
for (const question of questions) {
|
|
891
|
+
const answer = answers[question.question];
|
|
892
|
+
if (!answer)
|
|
893
|
+
continue;
|
|
894
|
+
const chosen = question.options.filter((option) => answer.split(", ").includes(option.label));
|
|
895
|
+
if (chosen.length !== 1 || !chosen[0]?.preview)
|
|
896
|
+
continue;
|
|
897
|
+
annotations[question.question] = { preview: chosen[0].preview };
|
|
898
|
+
}
|
|
899
|
+
return annotations;
|
|
900
|
+
}
|
|
649
901
|
var USAGE_MIN_INTERVAL_MS = 2e4;
|
|
650
902
|
var ClaudeSession = class extends EventEmitter {
|
|
651
903
|
id;
|
|
@@ -653,10 +905,47 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
653
905
|
cwd;
|
|
654
906
|
journal = new Journal();
|
|
655
907
|
#query = null;
|
|
908
|
+
/**
|
|
909
|
+
* Разговор, который был до нас, — прочитанный транскрипт продолженной сессии.
|
|
910
|
+
*
|
|
911
|
+
* Читается один раз, на старте, и больше не меняется: всё, что случится
|
|
912
|
+
* дальше, попадает в журнал. Промис, а не массив, потому что чтение
|
|
913
|
+
* асинхронное, а спросить историю могут раньше, чем оно закончится, — тогда
|
|
914
|
+
* запрос дождётся того же чтения вместо второго такого же. `null` — сессия
|
|
915
|
+
* начата с нуля, и продолжать нечего.
|
|
916
|
+
*/
|
|
917
|
+
#history = null;
|
|
656
918
|
/** Чем подняли эту сессию — нужно, чтобы объяснить провал старта. */
|
|
657
919
|
#executable = { source: "bundled" };
|
|
658
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;
|
|
659
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;
|
|
660
949
|
#capabilities = null;
|
|
661
950
|
#pendingPermissions = /* @__PURE__ */ new Map();
|
|
662
951
|
#model = null;
|
|
@@ -755,6 +1044,16 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
755
1044
|
get usage() {
|
|
756
1045
|
return this.#usage;
|
|
757
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Разговор, который был до старта этой сессии, — целиком и по порядку.
|
|
1049
|
+
*
|
|
1050
|
+
* Пустой массив у сессии, начатой с нуля. Ждёт чтения транскрипта, если оно
|
|
1051
|
+
* ещё идёт: клиент спрашивает историю сразу после открытия сессии, и ответ
|
|
1052
|
+
* «пока пусто» он принял бы за «истории нет».
|
|
1053
|
+
*/
|
|
1054
|
+
async history() {
|
|
1055
|
+
return await this.#history ?? [];
|
|
1056
|
+
}
|
|
758
1057
|
summary() {
|
|
759
1058
|
return {
|
|
760
1059
|
id: this.id,
|
|
@@ -764,6 +1063,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
764
1063
|
parentSessionId: this.parentSessionId,
|
|
765
1064
|
title: this.#title,
|
|
766
1065
|
state: this.#state,
|
|
1066
|
+
lastTurn: this.#lastTurn,
|
|
767
1067
|
model: this.#model,
|
|
768
1068
|
permissionMode: this.#permissionMode,
|
|
769
1069
|
effort: this.#effort,
|
|
@@ -782,6 +1082,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
782
1082
|
start() {
|
|
783
1083
|
if (this.#query)
|
|
784
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
|
+
}
|
|
785
1089
|
const options2 = {
|
|
786
1090
|
cwd: this.cwd,
|
|
787
1091
|
// Без этого SDK не читает с диска ничего: ни CLAUDE.md, ни кастомные
|
|
@@ -891,6 +1195,8 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
891
1195
|
});
|
|
892
1196
|
return;
|
|
893
1197
|
}
|
|
1198
|
+
this.#lastTurn = null;
|
|
1199
|
+
this.#interruptRequested = false;
|
|
894
1200
|
this.#setState("running");
|
|
895
1201
|
this.#inbox.push({
|
|
896
1202
|
type: "user",
|
|
@@ -898,17 +1204,46 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
898
1204
|
parent_tool_use_id: null
|
|
899
1205
|
});
|
|
900
1206
|
}
|
|
901
|
-
|
|
1207
|
+
/**
|
|
1208
|
+
* Ответ человека на запрос прав — он же ответ на вопрос агента.
|
|
1209
|
+
*
|
|
1210
|
+
* `answers` осмысленны только для `AskUserQuestion`: там разрешение и есть
|
|
1211
|
+
* ответ, и выбранное надо вернуть инструменту в его же аргументах. Схема
|
|
1212
|
+
* аргументов у него строгая (`strictObject`), поэтому дописываем ровно те
|
|
1213
|
+
* поля, которые она знает, — лишнее уронило бы вызов ZodError'ом там, где
|
|
1214
|
+
* человек только что нажал кнопку.
|
|
1215
|
+
*/
|
|
1216
|
+
respondPermission(requestId, decision, reason, answers, annotations) {
|
|
902
1217
|
const pending = this.#pendingPermissions.get(requestId);
|
|
903
1218
|
if (!pending)
|
|
904
1219
|
return;
|
|
905
1220
|
this.#pendingPermissions.delete(requestId);
|
|
906
|
-
|
|
907
|
-
|
|
1221
|
+
const answered = pending.questions && answers ? answers : null;
|
|
1222
|
+
let input = pending.input;
|
|
1223
|
+
if (answered) {
|
|
1224
|
+
const withPreviews = {
|
|
1225
|
+
...previewAnnotations(pending.questions ?? [], answered),
|
|
1226
|
+
...annotations
|
|
1227
|
+
};
|
|
1228
|
+
input = {
|
|
1229
|
+
...pending.input,
|
|
1230
|
+
answers: answered,
|
|
1231
|
+
...Object.keys(withPreviews).length > 0 ? { annotations: withPreviews } : {}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
pending.resolve(decision === "allow" ? { behavior: "allow", updatedInput: input } : { behavior: "deny", message: reason ?? "\u041E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u043C" });
|
|
1235
|
+
this.#record({
|
|
1236
|
+
kind: "permission.resolved",
|
|
1237
|
+
requestId,
|
|
1238
|
+
decision,
|
|
1239
|
+
reason,
|
|
1240
|
+
...answered ? { answers: answered } : {}
|
|
1241
|
+
});
|
|
908
1242
|
if (this.#pendingPermissions.size === 0)
|
|
909
1243
|
this.#setState("running");
|
|
910
1244
|
}
|
|
911
1245
|
async interrupt() {
|
|
1246
|
+
this.#interruptRequested = true;
|
|
912
1247
|
await this.#query?.interrupt();
|
|
913
1248
|
}
|
|
914
1249
|
/**
|
|
@@ -1115,7 +1450,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1115
1450
|
const pass = { continue: true };
|
|
1116
1451
|
if (!this.workspaceId)
|
|
1117
1452
|
return pass;
|
|
1118
|
-
const roots = this.#options.writableRoots
|
|
1453
|
+
const roots = this.#options.writableRoots ? this.#options.writableRoots() : [this.cwd];
|
|
1119
1454
|
if (roots === null)
|
|
1120
1455
|
return pass;
|
|
1121
1456
|
const target = writeTarget(input.tool_input);
|
|
@@ -1135,7 +1470,23 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1135
1470
|
}
|
|
1136
1471
|
};
|
|
1137
1472
|
};
|
|
1138
|
-
|
|
1473
|
+
/**
|
|
1474
|
+
* Мост между `canUseTool` из SDK и решением человека в UI.
|
|
1475
|
+
*
|
|
1476
|
+
* Сюда же приходит и `AskUserQuestion` — вопрос агента с вариантами ответа.
|
|
1477
|
+
* Отдельного канала для него в SDK нет и не нужно: CLI объявляет этот
|
|
1478
|
+
* инструмент как `requiresUserInteraction`, а такой всегда попадает в
|
|
1479
|
+
* `canUseTool`, в любом режиме прав, — даже в `bypassPermissions`, где всё
|
|
1480
|
+
* остальное проходит молча. Проверка стоит раньше разбора режима, так что
|
|
1481
|
+
* вопрос не потеряется ни в одном из них.
|
|
1482
|
+
*
|
|
1483
|
+
* Разрешение для него значит не «выполняй», а «вот ответ»: выбранное
|
|
1484
|
+
* возвращается в `updatedInput.answers`, и уже с ним инструмент выполняется
|
|
1485
|
+
* и складывает ответ в результат вызова. Пустой `answers` (то есть простое
|
|
1486
|
+
* «Разрешить») инструмент понимает как «человек не стал отвечать» — ровно
|
|
1487
|
+
* это и происходило, пока вопросы показывались обычной карточкой прав.
|
|
1488
|
+
*/
|
|
1489
|
+
#canUseTool = (toolName, input, { signal, decisionReason, toolUseID }) => {
|
|
1139
1490
|
if (this.#options.readOnly) {
|
|
1140
1491
|
this.#record({
|
|
1141
1492
|
kind: "error",
|
|
@@ -1147,8 +1498,9 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1147
1498
|
});
|
|
1148
1499
|
}
|
|
1149
1500
|
const requestId = randomUUID();
|
|
1501
|
+
const questions = toolName === ASK_QUESTION_TOOL ? parseQuestions(input) : null;
|
|
1150
1502
|
return new Promise((resolve) => {
|
|
1151
|
-
this.#pendingPermissions.set(requestId, { resolve, toolName, input });
|
|
1503
|
+
this.#pendingPermissions.set(requestId, { resolve, toolName, input, questions });
|
|
1152
1504
|
this.#setState("waiting_permission");
|
|
1153
1505
|
this.#record({
|
|
1154
1506
|
kind: "permission.request",
|
|
@@ -1157,8 +1509,11 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1157
1509
|
input,
|
|
1158
1510
|
// Причину даёт не всякий запрос: обычный вопрос в режиме `default`
|
|
1159
1511
|
// объясняется самим режимом, а вот запрос, поднятый хуком поверх
|
|
1160
|
-
// разрешающего режима, без неё выглядит сбоем.
|
|
1161
|
-
|
|
1512
|
+
// разрешающего режима, без неё выглядит сбоем. Вопросу агента причина
|
|
1513
|
+
// не нужна вовсе: CLI объясняет его технически («requiresUserInteraction»),
|
|
1514
|
+
// а спрашивают тут не «почему», а «что выбрать».
|
|
1515
|
+
...decisionReason && !questions ? { reason: decisionReason } : {},
|
|
1516
|
+
...questions ? { questions, toolUseId: toolUseID } : {}
|
|
1162
1517
|
});
|
|
1163
1518
|
signal.addEventListener("abort", () => {
|
|
1164
1519
|
if (!this.#pendingPermissions.delete(requestId))
|
|
@@ -1207,6 +1562,10 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1207
1562
|
this.#handleSystem(message);
|
|
1208
1563
|
break;
|
|
1209
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
|
+
}
|
|
1210
1569
|
for (const block of message.message.content) {
|
|
1211
1570
|
const blockId = `${message.message.id}:${this.#nextBlockIndex(message.message.id)}`;
|
|
1212
1571
|
if (block.type === "text") {
|
|
@@ -1250,6 +1609,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1250
1609
|
durationMs: message.duration_ms,
|
|
1251
1610
|
costUsd: message.total_cost_usd ?? null,
|
|
1252
1611
|
usage,
|
|
1612
|
+
contextTokens: this.#contextTokens,
|
|
1253
1613
|
text: "result" in message ? message.result : null
|
|
1254
1614
|
});
|
|
1255
1615
|
if (this.#options.outputFormat) {
|
|
@@ -1258,7 +1618,12 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1258
1618
|
text: "result" in message ? message.result : null
|
|
1259
1619
|
});
|
|
1260
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";
|
|
1261
1624
|
this.#setState("idle");
|
|
1625
|
+
if (alreadyIdle)
|
|
1626
|
+
this.emit("summary", this.summary());
|
|
1262
1627
|
if (this.#unpersistedTitle)
|
|
1263
1628
|
void this.#persistTitle();
|
|
1264
1629
|
void this.refreshUsage();
|
|
@@ -1371,7 +1736,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1371
1736
|
this.#record({
|
|
1372
1737
|
kind: "tool.result",
|
|
1373
1738
|
toolUseId: block.tool_use_id,
|
|
1374
|
-
content:
|
|
1739
|
+
content: contentToText2(block.content),
|
|
1375
1740
|
isError: block.is_error === true
|
|
1376
1741
|
});
|
|
1377
1742
|
}
|
|
@@ -1452,7 +1817,7 @@ function toUsageWindow(raw) {
|
|
|
1452
1817
|
resetsAt: typeof raw.resets_at === "string" ? raw.resets_at : ""
|
|
1453
1818
|
};
|
|
1454
1819
|
}
|
|
1455
|
-
function
|
|
1820
|
+
function contentToText2(content) {
|
|
1456
1821
|
if (typeof content === "string")
|
|
1457
1822
|
return content;
|
|
1458
1823
|
if (!Array.isArray(content))
|
|
@@ -1833,9 +2198,9 @@ function parsePatch(raw) {
|
|
|
1833
2198
|
body = "";
|
|
1834
2199
|
}
|
|
1835
2200
|
total += body.length;
|
|
1836
|
-
const
|
|
2201
|
+
const path17 = status === "deleted" ? oldPath : newPath || oldPath;
|
|
1837
2202
|
files.push({
|
|
1838
|
-
path:
|
|
2203
|
+
path: path17,
|
|
1839
2204
|
oldPath: status === "renamed" ? oldPath : "",
|
|
1840
2205
|
status,
|
|
1841
2206
|
added: count(body, "+"),
|
|
@@ -1847,10 +2212,10 @@ function parsePatch(raw) {
|
|
|
1847
2212
|
return { files, truncated };
|
|
1848
2213
|
}
|
|
1849
2214
|
function stripPrefix(value) {
|
|
1850
|
-
const
|
|
1851
|
-
if (
|
|
2215
|
+
const path17 = unquote(value.trim());
|
|
2216
|
+
if (path17 === "/dev/null")
|
|
1852
2217
|
return "";
|
|
1853
|
-
return
|
|
2218
|
+
return path17.replace(/^[ab]\//, "");
|
|
1854
2219
|
}
|
|
1855
2220
|
function count(body, mark) {
|
|
1856
2221
|
let n = 0;
|
|
@@ -2423,28 +2788,166 @@ function isRecord(value) {
|
|
|
2423
2788
|
return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
|
|
2424
2789
|
}
|
|
2425
2790
|
|
|
2426
|
-
// ../host/dist/
|
|
2791
|
+
// ../host/dist/pins.js
|
|
2427
2792
|
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
2428
|
-
import { homedir as homedir3 } from "node:os";
|
|
2429
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";
|
|
2430
2912
|
var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
2431
2913
|
var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
|
|
2914
|
+
var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
|
|
2432
2915
|
var FOLDER_ACCESS = ["all", "projects", "project"];
|
|
2916
|
+
var PERMISSION_MODES = [
|
|
2917
|
+
"default",
|
|
2918
|
+
"acceptEdits",
|
|
2919
|
+
"plan",
|
|
2920
|
+
"dontAsk",
|
|
2921
|
+
"bypassPermissions",
|
|
2922
|
+
"auto"
|
|
2923
|
+
];
|
|
2433
2924
|
var DEFAULT_APP_SETTINGS = {
|
|
2434
2925
|
autoArchiveAfterMs: DEFAULT_AUTO_ARCHIVE_AFTER_MS,
|
|
2435
2926
|
// Строгий режим по умолчанию: молча писать в чужой проект приложение начнёт
|
|
2436
2927
|
// только после того, как об этом попросили явно.
|
|
2437
|
-
folderAccess: "project"
|
|
2928
|
+
folderAccess: "project",
|
|
2929
|
+
// Выключено по умолчанию: сохранение — явное действие, пока человек сам не
|
|
2930
|
+
// попросил иначе.
|
|
2931
|
+
autoSaveAfterPauseMs: 0,
|
|
2932
|
+
// Модель за пользователя не выбираем: пока он не сказал иначе, сессия
|
|
2933
|
+
// поднимается ровно с той, с которой её поднял бы консольный Claude Code.
|
|
2934
|
+
startModel: null,
|
|
2935
|
+
// Тот же режим, с которым сессия стартовала до появления этой настройки.
|
|
2936
|
+
startPermissionMode: "default",
|
|
2937
|
+
// Терминал по умолчанию закрыт — его включают, а не выключают: приложение,
|
|
2938
|
+
// открытое в браузере, не обязано с первого запуска уметь запускать на этой
|
|
2939
|
+
// машине что угодно.
|
|
2940
|
+
terminalEnabled: false
|
|
2438
2941
|
};
|
|
2439
2942
|
var AppSettingsStore = class {
|
|
2440
2943
|
#settings = { ...DEFAULT_APP_SETTINGS };
|
|
2441
2944
|
#storePath;
|
|
2442
2945
|
constructor(dataDir) {
|
|
2443
|
-
this.#storePath =
|
|
2946
|
+
this.#storePath = path10.join(dataDir, "app-settings.json");
|
|
2444
2947
|
}
|
|
2445
2948
|
async load() {
|
|
2446
2949
|
try {
|
|
2447
|
-
const raw = await
|
|
2950
|
+
const raw = await readFile7(this.#storePath, "utf8");
|
|
2448
2951
|
this.#settings = sanitize(JSON.parse(raw), DEFAULT_APP_SETTINGS);
|
|
2449
2952
|
} catch {
|
|
2450
2953
|
this.#settings = { ...DEFAULT_APP_SETTINGS };
|
|
@@ -2464,8 +2967,8 @@ var AppSettingsStore = class {
|
|
|
2464
2967
|
async update(patch) {
|
|
2465
2968
|
const next = sanitize(patch, this.#settings);
|
|
2466
2969
|
this.#settings = next;
|
|
2467
|
-
await
|
|
2468
|
-
await
|
|
2970
|
+
await mkdir5(path10.dirname(this.#storePath), { recursive: true });
|
|
2971
|
+
await writeFile5(this.#storePath, JSON.stringify(next, null, 2), "utf8");
|
|
2469
2972
|
return this.current;
|
|
2470
2973
|
}
|
|
2471
2974
|
};
|
|
@@ -2478,14 +2981,32 @@ function sanitize(value, fallback) {
|
|
|
2478
2981
|
if (typeof after === "number" && Number.isFinite(after)) {
|
|
2479
2982
|
result.autoArchiveAfterMs = after <= 0 ? 0 : Math.max(Math.round(after), MIN_AUTO_ARCHIVE_AFTER_MS);
|
|
2480
2983
|
}
|
|
2481
|
-
const
|
|
2482
|
-
if (typeof
|
|
2483
|
-
result.folderAccess =
|
|
2984
|
+
const access3 = record.folderAccess;
|
|
2985
|
+
if (typeof access3 === "string" && FOLDER_ACCESS.includes(access3)) {
|
|
2986
|
+
result.folderAccess = access3;
|
|
2987
|
+
}
|
|
2988
|
+
const autoSave = record.autoSaveAfterPauseMs;
|
|
2989
|
+
if (typeof autoSave === "number" && Number.isFinite(autoSave)) {
|
|
2990
|
+
result.autoSaveAfterPauseMs = autoSave <= 0 ? 0 : Math.max(Math.round(autoSave), MIN_AUTO_SAVE_AFTER_PAUSE_MS);
|
|
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;
|
|
2484
3005
|
}
|
|
2485
3006
|
return result;
|
|
2486
3007
|
}
|
|
2487
3008
|
function claudeHome() {
|
|
2488
|
-
return
|
|
3009
|
+
return path10.join(homedir3(), ".claude");
|
|
2489
3010
|
}
|
|
2490
3011
|
function writableRoots(mode, cwd, workspacePaths) {
|
|
2491
3012
|
if (mode === "all")
|
|
@@ -2495,7 +3016,7 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
2495
3016
|
return [...roots];
|
|
2496
3017
|
for (const workspace of [cwd, ...workspacePaths]) {
|
|
2497
3018
|
roots.add(workspace);
|
|
2498
|
-
const parent =
|
|
3019
|
+
const parent = path10.dirname(workspace);
|
|
2499
3020
|
if (parent !== workspace)
|
|
2500
3021
|
roots.add(parent);
|
|
2501
3022
|
}
|
|
@@ -2503,9 +3024,9 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
2503
3024
|
}
|
|
2504
3025
|
|
|
2505
3026
|
// ../host/dist/plugins.js
|
|
2506
|
-
import { readFile as
|
|
3027
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2507
3028
|
import { homedir as homedir4 } from "node:os";
|
|
2508
|
-
import
|
|
3029
|
+
import path11 from "node:path";
|
|
2509
3030
|
var TIMEOUT_MS2 = 12e4;
|
|
2510
3031
|
var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
|
|
2511
3032
|
var PluginCli = class {
|
|
@@ -2604,10 +3125,10 @@ async function readCatalog(markets, installed) {
|
|
|
2604
3125
|
return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
2605
3126
|
}
|
|
2606
3127
|
async function readMarketplace(market, installed) {
|
|
2607
|
-
const root = market.installLocation ||
|
|
3128
|
+
const root = market.installLocation || path11.join(homedir4(), ".claude", "plugins", "marketplaces", market.name);
|
|
2608
3129
|
let manifest;
|
|
2609
3130
|
try {
|
|
2610
|
-
const text = await
|
|
3131
|
+
const text = await readFile8(path11.join(root, ".claude-plugin", "marketplace.json"), "utf8");
|
|
2611
3132
|
manifest = JSON.parse(text);
|
|
2612
3133
|
} catch {
|
|
2613
3134
|
return [];
|
|
@@ -2818,8 +3339,8 @@ function describe3(error) {
|
|
|
2818
3339
|
|
|
2819
3340
|
// ../host/dist/relay-link.js
|
|
2820
3341
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
2821
|
-
import { readFile as
|
|
2822
|
-
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";
|
|
2823
3344
|
import { hostname } from "node:os";
|
|
2824
3345
|
import WebSocket from "ws";
|
|
2825
3346
|
|
|
@@ -2841,13 +3362,26 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2841
3362
|
#clients = /* @__PURE__ */ new Set();
|
|
2842
3363
|
#error = null;
|
|
2843
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 };
|
|
2844
3378
|
/** Куда сопрягаться, пока не сопряглись. */
|
|
2845
3379
|
#defaultUrl;
|
|
2846
3380
|
#file;
|
|
2847
3381
|
#log;
|
|
2848
3382
|
constructor(options2) {
|
|
2849
3383
|
super();
|
|
2850
|
-
this.#file =
|
|
3384
|
+
this.#file = path12.join(options2.dataDir, "relay.json");
|
|
2851
3385
|
this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
|
|
2852
3386
|
this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
|
|
2853
3387
|
}
|
|
@@ -2862,9 +3396,57 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2862
3396
|
name: this.#credentials?.name ?? null,
|
|
2863
3397
|
hostId: this.#credentials?.hostId ?? null,
|
|
2864
3398
|
clients: this.#clients.size,
|
|
3399
|
+
shell: this.#shell,
|
|
2865
3400
|
error: this.#error
|
|
2866
3401
|
};
|
|
2867
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
|
+
}
|
|
2868
3450
|
#changed() {
|
|
2869
3451
|
this.emit("state");
|
|
2870
3452
|
}
|
|
@@ -2880,7 +3462,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2880
3462
|
}
|
|
2881
3463
|
async #read() {
|
|
2882
3464
|
try {
|
|
2883
|
-
const raw = await
|
|
3465
|
+
const raw = await readFile9(this.#file, "utf8");
|
|
2884
3466
|
const parsed = JSON.parse(raw);
|
|
2885
3467
|
if (typeof parsed !== "object" || parsed === null)
|
|
2886
3468
|
return null;
|
|
@@ -2893,8 +3475,8 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2893
3475
|
}
|
|
2894
3476
|
}
|
|
2895
3477
|
async #write(credentials) {
|
|
2896
|
-
await
|
|
2897
|
-
await
|
|
3478
|
+
await mkdir6(path12.dirname(this.#file), { recursive: true });
|
|
3479
|
+
await writeFile6(this.#file, `${JSON.stringify(credentials, null, 2)}
|
|
2898
3480
|
`, {
|
|
2899
3481
|
encoding: "utf8",
|
|
2900
3482
|
mode: 384
|
|
@@ -2945,6 +3527,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
2945
3527
|
this.#credentials = null;
|
|
2946
3528
|
this.#error = null;
|
|
2947
3529
|
this.#clients.clear();
|
|
3530
|
+
this.#shell = { enabled: false, pending: false, email: null, confirmedAt: null };
|
|
2948
3531
|
await rm2(this.#file, { force: true });
|
|
2949
3532
|
this.#changed();
|
|
2950
3533
|
this.#log("\u0441\u043E\u043F\u0440\u044F\u0436\u0435\u043D\u0438\u0435 \u0441\u043D\u044F\u0442\u043E");
|
|
@@ -3050,6 +3633,16 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
3050
3633
|
case "client.frame":
|
|
3051
3634
|
this.emit("frame", frame.clientId, frame.body);
|
|
3052
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;
|
|
3053
3646
|
case "ping":
|
|
3054
3647
|
socket.send(JSON.stringify({ t: "pong", nonce: frame.nonce }));
|
|
3055
3648
|
return;
|
|
@@ -3090,11 +3683,196 @@ function describe4(error) {
|
|
|
3090
3683
|
return error instanceof Error ? error.message : String(error);
|
|
3091
3684
|
}
|
|
3092
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
|
+
|
|
3093
3871
|
// ../host/dist/mcp-config.js
|
|
3094
|
-
import { readFile as
|
|
3095
|
-
import
|
|
3872
|
+
import { readFile as readFile10, writeFile as writeFile7, mkdir as mkdir7 } from "node:fs/promises";
|
|
3873
|
+
import path14 from "node:path";
|
|
3096
3874
|
var MCP_FILE = ".mcp.json";
|
|
3097
|
-
var LOCAL_SETTINGS2 =
|
|
3875
|
+
var LOCAL_SETTINGS2 = path14.join(".claude", "settings.local.json");
|
|
3098
3876
|
var queues = /* @__PURE__ */ new Map();
|
|
3099
3877
|
function serialize(cwd, work) {
|
|
3100
3878
|
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
@@ -3103,7 +3881,7 @@ function serialize(cwd, work) {
|
|
|
3103
3881
|
return next;
|
|
3104
3882
|
}
|
|
3105
3883
|
async function readProjectServers(cwd) {
|
|
3106
|
-
const file = await readJson(
|
|
3884
|
+
const file = await readJson(path14.join(cwd, MCP_FILE));
|
|
3107
3885
|
const servers = file?.mcpServers;
|
|
3108
3886
|
if (!servers || typeof servers !== "object")
|
|
3109
3887
|
return {};
|
|
@@ -3111,23 +3889,23 @@ async function readProjectServers(cwd) {
|
|
|
3111
3889
|
}
|
|
3112
3890
|
function addProjectServer(cwd, name, config) {
|
|
3113
3891
|
return serialize(cwd, async () => {
|
|
3114
|
-
const file = await readJson(
|
|
3892
|
+
const file = await readJson(path14.join(cwd, MCP_FILE)) ?? {};
|
|
3115
3893
|
const servers = file.mcpServers ?? {};
|
|
3116
3894
|
servers[name] = config;
|
|
3117
3895
|
file.mcpServers = servers;
|
|
3118
|
-
await writeJson(
|
|
3896
|
+
await writeJson(path14.join(cwd, MCP_FILE), file);
|
|
3119
3897
|
await approve(cwd, name);
|
|
3120
3898
|
});
|
|
3121
3899
|
}
|
|
3122
3900
|
function removeProjectServer(cwd, name) {
|
|
3123
3901
|
return serialize(cwd, async () => {
|
|
3124
|
-
const file = await readJson(
|
|
3902
|
+
const file = await readJson(path14.join(cwd, MCP_FILE));
|
|
3125
3903
|
const servers = file?.mcpServers;
|
|
3126
3904
|
if (!servers || !(name in servers))
|
|
3127
3905
|
return false;
|
|
3128
3906
|
delete servers[name];
|
|
3129
|
-
await writeJson(
|
|
3130
|
-
const settingsPath =
|
|
3907
|
+
await writeJson(path14.join(cwd, MCP_FILE), file);
|
|
3908
|
+
const settingsPath = path14.join(cwd, LOCAL_SETTINGS2);
|
|
3131
3909
|
const settings = await readJson(settingsPath);
|
|
3132
3910
|
const enabled = settings?.enabledMcpjsonServers;
|
|
3133
3911
|
if (settings && Array.isArray(enabled) && enabled.includes(name)) {
|
|
@@ -3138,7 +3916,7 @@ function removeProjectServer(cwd, name) {
|
|
|
3138
3916
|
});
|
|
3139
3917
|
}
|
|
3140
3918
|
async function approve(cwd, name) {
|
|
3141
|
-
const file =
|
|
3919
|
+
const file = path14.join(cwd, LOCAL_SETTINGS2);
|
|
3142
3920
|
const settings = await readJson(file) ?? {};
|
|
3143
3921
|
const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
3144
3922
|
if (!enabled.includes(name))
|
|
@@ -3177,31 +3955,34 @@ function describeConfig(config) {
|
|
|
3177
3955
|
}
|
|
3178
3956
|
async function readJson(file) {
|
|
3179
3957
|
try {
|
|
3180
|
-
const raw = await
|
|
3958
|
+
const raw = await readFile10(file, "utf8");
|
|
3181
3959
|
const parsed = JSON.parse(raw);
|
|
3182
3960
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
3183
3961
|
} catch (error) {
|
|
3184
3962
|
if (error.code === "ENOENT")
|
|
3185
3963
|
return null;
|
|
3186
3964
|
if (error instanceof SyntaxError) {
|
|
3187
|
-
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}`);
|
|
3188
3966
|
}
|
|
3189
3967
|
throw error;
|
|
3190
3968
|
}
|
|
3191
3969
|
}
|
|
3192
3970
|
async function writeJson(file, data) {
|
|
3193
|
-
await
|
|
3194
|
-
await
|
|
3971
|
+
await mkdir7(path14.dirname(file), { recursive: true });
|
|
3972
|
+
await writeFile7(file, `${JSON.stringify(data, null, 2)}
|
|
3195
3973
|
`, "utf8");
|
|
3196
3974
|
}
|
|
3197
3975
|
|
|
3198
3976
|
// ../host/dist/server.js
|
|
3199
3977
|
var SIDE_TITLE = "\xB7 \u0432\u043E\u043F\u0440\u043E\u0441 \u043D\u0430 \u043F\u043E\u043B\u044F\u0445";
|
|
3200
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;
|
|
3201
3981
|
var HostServer = class {
|
|
3202
3982
|
#options;
|
|
3203
3983
|
#workspaces;
|
|
3204
3984
|
#archive;
|
|
3985
|
+
#pins;
|
|
3205
3986
|
#settings;
|
|
3206
3987
|
#finder;
|
|
3207
3988
|
#sessions = /* @__PURE__ */ new Map();
|
|
@@ -3219,18 +4000,21 @@ var HostServer = class {
|
|
|
3219
4000
|
#clients = /* @__PURE__ */ new Set();
|
|
3220
4001
|
/** Удалённые вкладки по их id в relay — чтобы адресовать ответ именно той. */
|
|
3221
4002
|
#remotes = /* @__PURE__ */ new Map();
|
|
3222
|
-
#hostId =
|
|
4003
|
+
#hostId = randomUUID3();
|
|
3223
4004
|
#plugins = new PluginCli();
|
|
3224
4005
|
#auth = new AuthCli();
|
|
3225
4006
|
#relay;
|
|
4007
|
+
#terminals = new TerminalHub();
|
|
3226
4008
|
constructor(options2) {
|
|
3227
4009
|
this.#options = options2;
|
|
3228
4010
|
this.#workspaces = new WorkspaceRegistry(options2.dataDir);
|
|
3229
4011
|
this.#archive = new SessionArchive(options2.dataDir);
|
|
4012
|
+
this.#pins = new SessionPins(options2.dataDir);
|
|
3230
4013
|
this.#settings = new AppSettingsStore(options2.dataDir);
|
|
3231
4014
|
this.#finder = new ProjectFinder(this.#workspaces);
|
|
3232
4015
|
this.#relay = new RelayLink({ dataDir: options2.dataDir, defaultUrl: options2.relayUrl });
|
|
3233
4016
|
this.#wireRelay();
|
|
4017
|
+
this.#wireTerminals();
|
|
3234
4018
|
this.#auth.on("login", (state) => {
|
|
3235
4019
|
this.#broadcast({ type: "auth.login", state });
|
|
3236
4020
|
});
|
|
@@ -3241,6 +4025,7 @@ var HostServer = class {
|
|
|
3241
4025
|
async start() {
|
|
3242
4026
|
await this.#workspaces.load();
|
|
3243
4027
|
await this.#archive.load();
|
|
4028
|
+
await this.#pins.load();
|
|
3244
4029
|
await this.#settings.load();
|
|
3245
4030
|
await warmClaudeLauncher();
|
|
3246
4031
|
void this.#finder.warm();
|
|
@@ -3266,6 +4051,7 @@ var HostServer = class {
|
|
|
3266
4051
|
}
|
|
3267
4052
|
async shutdown() {
|
|
3268
4053
|
this.#auth.cancel();
|
|
4054
|
+
this.#terminals.closeAll();
|
|
3269
4055
|
await this.#relay.close();
|
|
3270
4056
|
await Promise.all([...this.#sessions.values()].map((s) => s.close("host shutdown")));
|
|
3271
4057
|
this.#sessions.clear();
|
|
@@ -3329,6 +4115,36 @@ var HostServer = class {
|
|
|
3329
4115
|
this.#broadcast({ type: "relay.state", state: this.#relay.state });
|
|
3330
4116
|
});
|
|
3331
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
|
+
}
|
|
3332
4148
|
#adoptRemote(clientId) {
|
|
3333
4149
|
const client = {
|
|
3334
4150
|
local: false,
|
|
@@ -3402,10 +4218,17 @@ var HostServer = class {
|
|
|
3402
4218
|
continue;
|
|
3403
4219
|
await this.#dropSession(session.id, "workspace closed");
|
|
3404
4220
|
}
|
|
4221
|
+
let unpinned = false;
|
|
4222
|
+
for (const id of gone) {
|
|
4223
|
+
if (await this.#pins.forgetWorkspace(id))
|
|
4224
|
+
unpinned = true;
|
|
4225
|
+
}
|
|
3405
4226
|
this.#broadcast({
|
|
3406
4227
|
type: "workspace.list",
|
|
3407
4228
|
workspaces: this.#workspaces.list()
|
|
3408
4229
|
});
|
|
4230
|
+
if (unpinned)
|
|
4231
|
+
await this.#sendPinned();
|
|
3409
4232
|
return;
|
|
3410
4233
|
}
|
|
3411
4234
|
case "workspace.find": {
|
|
@@ -3468,6 +4291,27 @@ var HostServer = class {
|
|
|
3468
4291
|
await this.#archive.unarchive(frame.sessionIds);
|
|
3469
4292
|
await this.#sendSessionList(frame.workspaceId);
|
|
3470
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;
|
|
3471
4315
|
case "session.rename":
|
|
3472
4316
|
await this.#renameSession(frame.sessionId, frame.title, frame.workspaceId);
|
|
3473
4317
|
return;
|
|
@@ -3494,6 +4338,21 @@ var HostServer = class {
|
|
|
3494
4338
|
}
|
|
3495
4339
|
return;
|
|
3496
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
|
+
}
|
|
3497
4356
|
case "usage.get": {
|
|
3498
4357
|
const session = this.#requireSession(frame.sessionId);
|
|
3499
4358
|
if (session.usage) {
|
|
@@ -3510,7 +4369,7 @@ var HostServer = class {
|
|
|
3510
4369
|
this.#requireSession(frame.sessionId).sendPrompt(frame.text);
|
|
3511
4370
|
return;
|
|
3512
4371
|
case "permission.respond":
|
|
3513
|
-
this.#requireSession(frame.sessionId).respondPermission(frame.requestId, frame.decision, frame.reason);
|
|
4372
|
+
this.#requireSession(frame.sessionId).respondPermission(frame.requestId, frame.decision, frame.reason, frame.answers, frame.annotations);
|
|
3514
4373
|
return;
|
|
3515
4374
|
case "side.ask":
|
|
3516
4375
|
this.#ensureSide(this.#requireSession(frame.sessionId)).sendPrompt(frame.text);
|
|
@@ -3715,6 +4574,54 @@ var HostServer = class {
|
|
|
3715
4574
|
}
|
|
3716
4575
|
return;
|
|
3717
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;
|
|
3718
4625
|
case "settings.get":
|
|
3719
4626
|
this.#send(socket, { type: "settings.state", settings: this.#settings.current });
|
|
3720
4627
|
return;
|
|
@@ -3727,6 +4634,12 @@ var HostServer = class {
|
|
|
3727
4634
|
await this.#sendSessionList(workspace.id);
|
|
3728
4635
|
}
|
|
3729
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
|
+
}
|
|
3730
4643
|
return;
|
|
3731
4644
|
}
|
|
3732
4645
|
case "auth.status":
|
|
@@ -3756,7 +4669,40 @@ var HostServer = class {
|
|
|
3756
4669
|
this.#requireLocal(socket, "\u041E\u0442\u0432\u044F\u0437\u0430\u0442\u044C \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440");
|
|
3757
4670
|
await this.#relay.unpair();
|
|
3758
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;
|
|
4684
|
+
}
|
|
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.");
|
|
3759
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.");
|
|
3760
4706
|
}
|
|
3761
4707
|
/**
|
|
3762
4708
|
* Настройки удалённого доступа меняются только с самой машины.
|
|
@@ -3890,6 +4836,32 @@ var HostServer = class {
|
|
|
3890
4836
|
async #sendSessionList(workspaceId) {
|
|
3891
4837
|
const { sessions, archived } = await this.#listSessions(workspaceId);
|
|
3892
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 });
|
|
3893
4865
|
}
|
|
3894
4866
|
async #startSession(socket, frame) {
|
|
3895
4867
|
const workspace = this.#workspaces.find(frame.workspaceId);
|
|
@@ -3899,13 +4871,14 @@ var HostServer = class {
|
|
|
3899
4871
|
const resumedTitle = resumed ? diskTitle(resumed) : null;
|
|
3900
4872
|
if (frame.resume)
|
|
3901
4873
|
await this.#archive.unarchive([frame.resume]);
|
|
4874
|
+
const startDefaults = this.#settings.current;
|
|
3902
4875
|
const session = new ClaudeSession({
|
|
3903
4876
|
workspaceId: workspace.id,
|
|
3904
4877
|
cwd: workspace.path,
|
|
3905
4878
|
resume: frame.resume,
|
|
3906
4879
|
fork: frame.fork,
|
|
3907
|
-
permissionMode: frame.permissionMode,
|
|
3908
|
-
model: frame.model,
|
|
4880
|
+
permissionMode: frame.permissionMode ?? startDefaults.startPermissionMode,
|
|
4881
|
+
model: frame.model ?? startDefaults.startModel ?? void 0,
|
|
3909
4882
|
...resumedTitle ? { title: resumedTitle } : {},
|
|
3910
4883
|
// Имя проекта вместо «Без названия»: свежую сессию в списке уже надо
|
|
3911
4884
|
// как-то называть, а сказать о ней пока нечего, кроме того, где она.
|
|
@@ -3929,6 +4902,9 @@ var HostServer = class {
|
|
|
3929
4902
|
});
|
|
3930
4903
|
session.on("summary", (summary) => {
|
|
3931
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);
|
|
3932
4908
|
});
|
|
3933
4909
|
session.on("capabilities", () => {
|
|
3934
4910
|
if (!session.capabilities)
|
|
@@ -4191,6 +5167,9 @@ var HostServer = class {
|
|
|
4191
5167
|
parentSessionId: null,
|
|
4192
5168
|
title: diskTitle(info),
|
|
4193
5169
|
state: "closed",
|
|
5170
|
+
// Хода в этой жизни у неё не было: сессия не поднята, и чем кончился
|
|
5171
|
+
// последний ход прошлой, знает только её JSONL.
|
|
5172
|
+
lastTurn: null,
|
|
4194
5173
|
model: null,
|
|
4195
5174
|
permissionMode: "default",
|
|
4196
5175
|
// Сессия с диска не поднята: чем она была настроена, знает только её
|
|
@@ -4204,10 +5183,14 @@ var HostServer = class {
|
|
|
4204
5183
|
lastSeq: 0,
|
|
4205
5184
|
totalCostUsd: 0
|
|
4206
5185
|
}));
|
|
4207
|
-
|
|
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);
|
|
4208
5189
|
const sessions = [];
|
|
4209
5190
|
const archived = [];
|
|
4210
|
-
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;
|
|
4211
5194
|
const record = this.#archive.find(summary.claudeSessionId ?? summary.id);
|
|
4212
5195
|
if (!record) {
|
|
4213
5196
|
sessions.push(summary);
|
|
@@ -4245,8 +5228,8 @@ var HostServer = class {
|
|
|
4245
5228
|
return;
|
|
4246
5229
|
}
|
|
4247
5230
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
4248
|
-
const requested =
|
|
4249
|
-
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");
|
|
4250
5233
|
if (!existsSync3(target)) {
|
|
4251
5234
|
res.writeHead(404).end("not found");
|
|
4252
5235
|
return;
|
|
@@ -4272,12 +5255,12 @@ function contentType(file) {
|
|
|
4272
5255
|
".svg": "image/svg+xml",
|
|
4273
5256
|
".woff2": "font/woff2"
|
|
4274
5257
|
};
|
|
4275
|
-
return types[
|
|
5258
|
+
return types[path15.extname(file)] ?? "application/octet-stream";
|
|
4276
5259
|
}
|
|
4277
5260
|
|
|
4278
5261
|
// dist/ccv.js
|
|
4279
|
-
var HERE =
|
|
4280
|
-
var PACKAGE_ROOT =
|
|
5262
|
+
var HERE = path16.dirname(fileURLToPath(import.meta.url));
|
|
5263
|
+
var PACKAGE_ROOT = path16.resolve(HERE, "..");
|
|
4281
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.
|
|
4282
5265
|
|
|
4283
5266
|
\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
|
|
@@ -4344,7 +5327,7 @@ var server = new HostServer({
|
|
|
4344
5327
|
dataDir: defaults.dataDir,
|
|
4345
5328
|
// UI лежит в пакете рядом с бандлом — это и есть разница между установленной
|
|
4346
5329
|
// утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
|
|
4347
|
-
webRoot: process.env.CCV_WEB_ROOT ??
|
|
5330
|
+
webRoot: process.env.CCV_WEB_ROOT ?? path16.join(PACKAGE_ROOT, "web"),
|
|
4348
5331
|
version,
|
|
4349
5332
|
relayUrl: defaults.relayUrl
|
|
4350
5333
|
});
|
|
@@ -4373,7 +5356,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
4373
5356
|
}
|
|
4374
5357
|
async function packageVersion() {
|
|
4375
5358
|
try {
|
|
4376
|
-
const text = await
|
|
5359
|
+
const text = await readFile11(path16.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
4377
5360
|
return JSON.parse(text).version ?? "0.0.0";
|
|
4378
5361
|
} catch {
|
|
4379
5362
|
return "0.0.0";
|