@i14ks/ccv 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/ccv.mjs +210 -77
- package/package.json +1 -1
- package/web/assets/{codemirror-D11GWMsB.js → codemirror-DAgbW3uz.js} +1 -1
- package/web/assets/{index-Iwl1w0_V.js → index-1j2ILa-W.js} +1 -1
- package/web/assets/{index-J2jvoYvL.js → index-BI8B49yl.js} +1 -1
- package/web/assets/{index-Bw0U50QY.js → index-BO3Em_j_.js} +1 -1
- package/web/assets/{index-CmCj0IE6.js → index-BTNpZcr3.js} +1 -1
- package/web/assets/{index-BIhkivIg.js → index-B_KTK-Ue.js} +1 -1
- package/web/assets/{index-4S0cl_O2.js → index-Bjh8yEB4.js} +1 -1
- package/web/assets/{index-D57ZXpBD.js → index-C0igZOa9.js} +1 -1
- package/web/assets/{index-CZF-u-hv.js → index-C11bbOpw.js} +1 -1
- package/web/assets/{index-xSo7_4Td.js → index-CBTNRCgC.js} +1 -1
- package/web/assets/{index-09MC_4AF.js → index-CFzKsiua.js} +1 -1
- package/web/assets/{index-Dn5EQ5YI.js → index-CIpZI228.js} +1 -1
- package/web/assets/index-CWCyiPjz.js +132 -0
- package/web/assets/{index-Czu0PbLS.js → index-Cd-2HW_i.js} +1 -1
- package/web/assets/{index-BDuAzkOQ.js → index-CquqZqWN.js} +1 -1
- package/web/assets/{index-BSEf83Uc.js → index-CzYpffog.js} +1 -1
- package/web/assets/{index-DO1FY4r1.js → index-D-pPtz2I.js} +1 -1
- package/web/assets/{index-DdCJlB8h.js → index-DDJ5NYOk.js} +1 -1
- package/web/assets/{index-Bg0VD211.js → index-DPG5pWPG.js} +1 -1
- package/web/assets/{index-DABElhgt.js → index-DmfDW_VA.js} +1 -1
- package/web/assets/{index-ZstREAcb.js → index-XkZY-Wpw.js} +1 -1
- package/web/assets/index-s-BhjU3m.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-9Toz507b.js +0 -132
- package/web/assets/index-D5y0Nk74.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 readFile15 } from "node:fs/promises";
|
|
6
|
+
import path20 from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
|
|
@@ -145,9 +145,9 @@ function envDefaults() {
|
|
|
145
145
|
|
|
146
146
|
// ../host/dist/server.js
|
|
147
147
|
import { createServer } from "node:http";
|
|
148
|
-
import { createReadStream, existsSync as
|
|
149
|
-
import
|
|
150
|
-
import { homedir as
|
|
148
|
+
import { createReadStream, existsSync as existsSync4, statSync as statSync3 } from "node:fs";
|
|
149
|
+
import path19 from "node:path";
|
|
150
|
+
import { homedir as homedir6 } from "node:os";
|
|
151
151
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
152
152
|
import { WebSocketServer } from "ws";
|
|
153
153
|
import { renameSession as renameSession2 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -164,6 +164,8 @@ function workspaceIdFromPath(absolutePath) {
|
|
|
164
164
|
// ../host/dist/session.js
|
|
165
165
|
import { EventEmitter } from "node:events";
|
|
166
166
|
import { randomUUID } from "node:crypto";
|
|
167
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
168
|
+
import { homedir as homedir3 } from "node:os";
|
|
167
169
|
import path5 from "node:path";
|
|
168
170
|
import { query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
169
171
|
|
|
@@ -1109,6 +1111,7 @@ function previewAnnotations(questions, answers) {
|
|
|
1109
1111
|
return annotations;
|
|
1110
1112
|
}
|
|
1111
1113
|
var USAGE_MIN_INTERVAL_MS = 2e4;
|
|
1114
|
+
var USAGE_TURN_INTERVAL_MS = 12e4;
|
|
1112
1115
|
var CONTEXT_MIN_INTERVAL_MS = 5e3;
|
|
1113
1116
|
var ClaudeSession = class extends EventEmitter {
|
|
1114
1117
|
id;
|
|
@@ -1864,6 +1867,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
1864
1867
|
const raw = await q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET();
|
|
1865
1868
|
this.#usage = toUsageState(raw);
|
|
1866
1869
|
this.emit("usage", this.#usage);
|
|
1870
|
+
writeFinisherSnapshot(this.#usage, this.#claudeSessionId, this.cwd);
|
|
1867
1871
|
} catch {
|
|
1868
1872
|
} finally {
|
|
1869
1873
|
this.#usageAt = Date.now();
|
|
@@ -2172,6 +2176,8 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2172
2176
|
break;
|
|
2173
2177
|
case "user":
|
|
2174
2178
|
this.#handleToolResults(message);
|
|
2179
|
+
if (Date.now() - this.#usageAt >= USAGE_TURN_INTERVAL_MS)
|
|
2180
|
+
void this.refreshUsage();
|
|
2175
2181
|
break;
|
|
2176
2182
|
case "stream_event":
|
|
2177
2183
|
this.#handleStreamEvent(message);
|
|
@@ -2291,7 +2297,7 @@ var ClaudeSession = class extends EventEmitter {
|
|
|
2291
2297
|
this.#capabilities = {
|
|
2292
2298
|
...this.#capabilities ?? this.#baseCapabilities(),
|
|
2293
2299
|
commands: init.commands.map(toSlashCommand),
|
|
2294
|
-
models: init.models.map((m) => ({
|
|
2300
|
+
models: init.models.filter((m) => isPlainModel(m.value)).map((m) => ({
|
|
2295
2301
|
value: m.value,
|
|
2296
2302
|
resolvedModel: m.resolvedModel ?? null,
|
|
2297
2303
|
displayName: m.displayName,
|
|
@@ -2399,6 +2405,9 @@ function isApiErrorMessage(message) {
|
|
|
2399
2405
|
function apiErrorReason(status) {
|
|
2400
2406
|
return status === void 0 ? "\u0437\u0430\u043F\u0440\u043E\u0441 \u043A API \u043D\u0435 \u0434\u043E\u0448\u0451\u043B" : `API \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${status}`;
|
|
2401
2407
|
}
|
|
2408
|
+
function isPlainModel(value) {
|
|
2409
|
+
return /^[a-z]+$/.test(value);
|
|
2410
|
+
}
|
|
2402
2411
|
function toSlashCommand(command) {
|
|
2403
2412
|
return {
|
|
2404
2413
|
name: command.name,
|
|
@@ -2418,6 +2427,27 @@ function toUsageState(raw) {
|
|
|
2418
2427
|
fetchedAt: Date.now()
|
|
2419
2428
|
};
|
|
2420
2429
|
}
|
|
2430
|
+
function writeFinisherSnapshot(usage, sessionId, cwd) {
|
|
2431
|
+
if (!usage.available)
|
|
2432
|
+
return;
|
|
2433
|
+
const dir = path5.join(homedir3(), ".claude", "finisher");
|
|
2434
|
+
if (!existsSync(dir))
|
|
2435
|
+
return;
|
|
2436
|
+
const snapshot = {
|
|
2437
|
+
updated_at: Date.now(),
|
|
2438
|
+
session_id: sessionId,
|
|
2439
|
+
cwd,
|
|
2440
|
+
rate_limits: {
|
|
2441
|
+
five_hour: usage.fiveHour?.utilization ?? null,
|
|
2442
|
+
seven_day: usage.sevenDay?.utilization ?? null,
|
|
2443
|
+
spend_limit: null
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
try {
|
|
2447
|
+
writeFileSync(path5.join(dir, "usage-cache.json"), JSON.stringify(snapshot, null, 2));
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2421
2451
|
function toUsageWindow(raw) {
|
|
2422
2452
|
if (!raw || typeof raw.utilization !== "number")
|
|
2423
2453
|
return null;
|
|
@@ -3018,9 +3048,9 @@ function parsePatch(raw) {
|
|
|
3018
3048
|
body = "";
|
|
3019
3049
|
}
|
|
3020
3050
|
total += body.length;
|
|
3021
|
-
const
|
|
3051
|
+
const path21 = status === "deleted" ? oldPath : newPath || oldPath;
|
|
3022
3052
|
files.push({
|
|
3023
|
-
path:
|
|
3053
|
+
path: path21,
|
|
3024
3054
|
oldPath: status === "renamed" ? oldPath : "",
|
|
3025
3055
|
status,
|
|
3026
3056
|
added: count(body, "+"),
|
|
@@ -3032,10 +3062,10 @@ function parsePatch(raw) {
|
|
|
3032
3062
|
return { files, truncated };
|
|
3033
3063
|
}
|
|
3034
3064
|
function stripPrefix(value) {
|
|
3035
|
-
const
|
|
3036
|
-
if (
|
|
3065
|
+
const path21 = unquote(value.trim());
|
|
3066
|
+
if (path21 === "/dev/null")
|
|
3037
3067
|
return "";
|
|
3038
|
-
return
|
|
3068
|
+
return path21.replace(/^[ab]\//, "");
|
|
3039
3069
|
}
|
|
3040
3070
|
function count(body, mark) {
|
|
3041
3071
|
let n = 0;
|
|
@@ -3277,7 +3307,7 @@ function describe3(error) {
|
|
|
3277
3307
|
}
|
|
3278
3308
|
|
|
3279
3309
|
// ../host/dist/finder.js
|
|
3280
|
-
import { existsSync, statSync } from "node:fs";
|
|
3310
|
+
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
3281
3311
|
import { readdir as readdir2 } from "node:fs/promises";
|
|
3282
3312
|
import os2 from "node:os";
|
|
3283
3313
|
import path8 from "node:path";
|
|
@@ -3465,7 +3495,7 @@ var ProjectFinder = class {
|
|
|
3465
3495
|
}
|
|
3466
3496
|
};
|
|
3467
3497
|
function usable(dir) {
|
|
3468
|
-
return !contains(os2.tmpdir(), dir) &&
|
|
3498
|
+
return !contains(os2.tmpdir(), dir) && existsSync2(dir);
|
|
3469
3499
|
}
|
|
3470
3500
|
function tokens(query2) {
|
|
3471
3501
|
return query2.toLowerCase().split(/[^a-z0-9а-яё]+/i).filter(Boolean);
|
|
@@ -3562,7 +3592,7 @@ function fromText(text) {
|
|
|
3562
3592
|
}
|
|
3563
3593
|
|
|
3564
3594
|
// ../host/dist/workspaces.js
|
|
3565
|
-
import { existsSync as
|
|
3595
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
|
|
3566
3596
|
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
3567
3597
|
import path9 from "node:path";
|
|
3568
3598
|
import { listSessions as listSessions2 } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -3586,7 +3616,7 @@ var WorkspaceRegistry = class {
|
|
|
3586
3616
|
if (isEntry(item))
|
|
3587
3617
|
return item;
|
|
3588
3618
|
return null;
|
|
3589
|
-
}).filter((e) => e !== null &&
|
|
3619
|
+
}).filter((e) => e !== null && existsSync3(e.path));
|
|
3590
3620
|
} catch {
|
|
3591
3621
|
this.#entries = [];
|
|
3592
3622
|
}
|
|
@@ -3609,7 +3639,7 @@ var WorkspaceRegistry = class {
|
|
|
3609
3639
|
}
|
|
3610
3640
|
async open(rawPath) {
|
|
3611
3641
|
const absolute = path9.resolve(rawPath);
|
|
3612
|
-
if (!
|
|
3642
|
+
if (!existsSync3(absolute)) {
|
|
3613
3643
|
throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${absolute}`);
|
|
3614
3644
|
}
|
|
3615
3645
|
const existing = this.#entries.find((e) => e.path === absolute);
|
|
@@ -3676,14 +3706,14 @@ var WorkspaceRegistry = class {
|
|
|
3676
3706
|
const name = nameRaw.trim();
|
|
3677
3707
|
requireName(name);
|
|
3678
3708
|
const parent = path9.resolve(parentRaw.trim());
|
|
3679
|
-
if (!
|
|
3709
|
+
if (!existsSync3(parent)) {
|
|
3680
3710
|
throw new Error(`\u041A\u0430\u0442\u0430\u043B\u043E\u0433 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: ${parent}`);
|
|
3681
3711
|
}
|
|
3682
3712
|
if (!statSync2(parent).isDirectory()) {
|
|
3683
3713
|
throw new Error(`\u042D\u0442\u043E \u043D\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433: ${parent}`);
|
|
3684
3714
|
}
|
|
3685
3715
|
const target = path9.join(parent, name);
|
|
3686
|
-
if (
|
|
3716
|
+
if (existsSync3(target)) {
|
|
3687
3717
|
throw new Error(`\u0423\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442: ${target} \u2014 \u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0435\u0433\u043E \u043A\u0430\u043A \u043F\u0440\u043E\u0435\u043A\u0442`);
|
|
3688
3718
|
}
|
|
3689
3719
|
await mkdir3(target);
|
|
@@ -3765,7 +3795,7 @@ function toInfo(entry) {
|
|
|
3765
3795
|
id: workspaceIdFromPath(absolute),
|
|
3766
3796
|
path: absolute,
|
|
3767
3797
|
name: path9.basename(absolute) || absolute,
|
|
3768
|
-
hasClaudeMd:
|
|
3798
|
+
hasClaudeMd: existsSync3(path9.join(absolute, "CLAUDE.md")),
|
|
3769
3799
|
...archivedAt ? { archivedAt } : {}
|
|
3770
3800
|
};
|
|
3771
3801
|
}
|
|
@@ -3981,9 +4011,79 @@ function isRecord2(value) {
|
|
|
3981
4011
|
return typeof value === "object" && value !== null && typeof value.workspaceId === "string" && typeof value.at === "number";
|
|
3982
4012
|
}
|
|
3983
4013
|
|
|
3984
|
-
// ../host/dist/
|
|
4014
|
+
// ../host/dist/order.js
|
|
3985
4015
|
import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
|
|
3986
4016
|
import path12 from "node:path";
|
|
4017
|
+
var ListOrderStore = class {
|
|
4018
|
+
#order = emptyOrder();
|
|
4019
|
+
#storePath;
|
|
4020
|
+
constructor(dataDir) {
|
|
4021
|
+
this.#storePath = path12.join(dataDir, "list-order.json");
|
|
4022
|
+
}
|
|
4023
|
+
async load() {
|
|
4024
|
+
try {
|
|
4025
|
+
const raw = await readFile9(this.#storePath, "utf8");
|
|
4026
|
+
this.#order = sanitize(JSON.parse(raw));
|
|
4027
|
+
} catch {
|
|
4028
|
+
this.#order = emptyOrder();
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
get current() {
|
|
4032
|
+
return {
|
|
4033
|
+
workspaces: [...this.#order.workspaces],
|
|
4034
|
+
sessions: Object.fromEntries(Object.entries(this.#order.sessions).map(([k, v]) => [k, [...v]])),
|
|
4035
|
+
pinned: [...this.#order.pinned],
|
|
4036
|
+
live: [...this.#order.live]
|
|
4037
|
+
};
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Записать порядок одного списка. `ids === null` — сбросить его: список
|
|
4041
|
+
* вернётся к обычному порядку.
|
|
4042
|
+
*/
|
|
4043
|
+
async set(scope, ids, workspaceId) {
|
|
4044
|
+
const clean = ids === null ? null : dedupe(ids);
|
|
4045
|
+
if (scope === "sessions") {
|
|
4046
|
+
if (!workspaceId)
|
|
4047
|
+
throw new Error("\u0414\u043B\u044F \u043F\u043E\u0440\u044F\u0434\u043A\u0430 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0443\u0436\u0435\u043D workspaceId");
|
|
4048
|
+
if (clean === null || clean.length === 0)
|
|
4049
|
+
delete this.#order.sessions[workspaceId];
|
|
4050
|
+
else
|
|
4051
|
+
this.#order.sessions[workspaceId] = clean;
|
|
4052
|
+
} else {
|
|
4053
|
+
this.#order[scope] = clean ?? [];
|
|
4054
|
+
}
|
|
4055
|
+
await mkdir6(path12.dirname(this.#storePath), { recursive: true });
|
|
4056
|
+
await writeFile7(this.#storePath, JSON.stringify(this.#order, null, 2), "utf8");
|
|
4057
|
+
}
|
|
4058
|
+
};
|
|
4059
|
+
function emptyOrder() {
|
|
4060
|
+
return { workspaces: [], sessions: {}, pinned: [], live: [] };
|
|
4061
|
+
}
|
|
4062
|
+
function dedupe(ids) {
|
|
4063
|
+
return [...new Set(ids.filter((id) => typeof id === "string" && id !== ""))];
|
|
4064
|
+
}
|
|
4065
|
+
function sanitize(value) {
|
|
4066
|
+
const result = emptyOrder();
|
|
4067
|
+
if (typeof value !== "object" || value === null)
|
|
4068
|
+
return result;
|
|
4069
|
+
const record = value;
|
|
4070
|
+
const list = (v) => Array.isArray(v) ? dedupe(v) : [];
|
|
4071
|
+
result.workspaces = list(record.workspaces);
|
|
4072
|
+
result.pinned = list(record.pinned);
|
|
4073
|
+
result.live = list(record.live);
|
|
4074
|
+
if (typeof record.sessions === "object" && record.sessions !== null) {
|
|
4075
|
+
for (const [workspaceId, ids] of Object.entries(record.sessions)) {
|
|
4076
|
+
const clean = list(ids);
|
|
4077
|
+
if (clean.length > 0)
|
|
4078
|
+
result.sessions[workspaceId] = clean;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
return result;
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
// ../host/dist/folders.js
|
|
4085
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
|
|
4086
|
+
import path13 from "node:path";
|
|
3987
4087
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3988
4088
|
var Folders = class {
|
|
3989
4089
|
#workspaceFolders = /* @__PURE__ */ new Map();
|
|
@@ -3994,11 +4094,11 @@ var Folders = class {
|
|
|
3994
4094
|
#pinnedAssignments = /* @__PURE__ */ new Map();
|
|
3995
4095
|
#storePath;
|
|
3996
4096
|
constructor(dataDir) {
|
|
3997
|
-
this.#storePath =
|
|
4097
|
+
this.#storePath = path13.join(dataDir, "folders.json");
|
|
3998
4098
|
}
|
|
3999
4099
|
async load() {
|
|
4000
4100
|
try {
|
|
4001
|
-
const raw = await
|
|
4101
|
+
const raw = await readFile10(this.#storePath, "utf8");
|
|
4002
4102
|
const parsed = JSON.parse(raw);
|
|
4003
4103
|
this.#workspaceFolders = new Map(Object.entries(parsed.workspaceFolders ?? {}).filter(([, r]) => isWorkspaceFolder(r)));
|
|
4004
4104
|
this.#sessionFolders = new Map(Object.entries(parsed.sessionFolders ?? {}).filter(([, r]) => isSessionFolder(r)));
|
|
@@ -4181,8 +4281,8 @@ var Folders = class {
|
|
|
4181
4281
|
sessionAssignments: Object.fromEntries(this.#sessionAssignments),
|
|
4182
4282
|
pinnedAssignments: Object.fromEntries(this.#pinnedAssignments)
|
|
4183
4283
|
};
|
|
4184
|
-
await
|
|
4185
|
-
await
|
|
4284
|
+
await mkdir7(path13.dirname(this.#storePath), { recursive: true });
|
|
4285
|
+
await writeFile8(this.#storePath, JSON.stringify(file, null, 2), "utf8");
|
|
4186
4286
|
}
|
|
4187
4287
|
};
|
|
4188
4288
|
function isWorkspaceFolder(value) {
|
|
@@ -4196,9 +4296,9 @@ function isPinnedFolder(value) {
|
|
|
4196
4296
|
}
|
|
4197
4297
|
|
|
4198
4298
|
// ../host/dist/app-settings.js
|
|
4199
|
-
import { mkdir as
|
|
4200
|
-
import { homedir as
|
|
4201
|
-
import
|
|
4299
|
+
import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
|
|
4300
|
+
import { homedir as homedir4 } from "node:os";
|
|
4301
|
+
import path14 from "node:path";
|
|
4202
4302
|
var DEFAULT_AUTO_ARCHIVE_AFTER_MS = 2 * 24 * 60 * 60 * 1e3;
|
|
4203
4303
|
var MIN_AUTO_ARCHIVE_AFTER_MS = 60 * 60 * 1e3;
|
|
4204
4304
|
var MIN_AUTO_SAVE_AFTER_PAUSE_MS = 500;
|
|
@@ -4227,18 +4327,20 @@ var DEFAULT_APP_SETTINGS = {
|
|
|
4227
4327
|
// Терминал по умолчанию закрыт — его включают, а не выключают: приложение,
|
|
4228
4328
|
// открытое в браузере, не обязано с первого запуска уметь запускать на этой
|
|
4229
4329
|
// машине что угодно.
|
|
4230
|
-
terminalEnabled: false
|
|
4330
|
+
terminalEnabled: false,
|
|
4331
|
+
// Выключено по умолчанию: списки идут в том порядке, в каком их расставили.
|
|
4332
|
+
liveSessionsFirst: false
|
|
4231
4333
|
};
|
|
4232
4334
|
var AppSettingsStore = class {
|
|
4233
4335
|
#settings = { ...DEFAULT_APP_SETTINGS };
|
|
4234
4336
|
#storePath;
|
|
4235
4337
|
constructor(dataDir) {
|
|
4236
|
-
this.#storePath =
|
|
4338
|
+
this.#storePath = path14.join(dataDir, "app-settings.json");
|
|
4237
4339
|
}
|
|
4238
4340
|
async load() {
|
|
4239
4341
|
try {
|
|
4240
|
-
const raw = await
|
|
4241
|
-
this.#settings =
|
|
4342
|
+
const raw = await readFile11(this.#storePath, "utf8");
|
|
4343
|
+
this.#settings = sanitize2(JSON.parse(raw), DEFAULT_APP_SETTINGS);
|
|
4242
4344
|
} catch {
|
|
4243
4345
|
this.#settings = { ...DEFAULT_APP_SETTINGS };
|
|
4244
4346
|
}
|
|
@@ -4255,14 +4357,14 @@ var AppSettingsStore = class {
|
|
|
4255
4357
|
* всё равно получит в ответ то, что реально записано, и покажет именно это.
|
|
4256
4358
|
*/
|
|
4257
4359
|
async update(patch) {
|
|
4258
|
-
const next =
|
|
4360
|
+
const next = sanitize2(patch, this.#settings);
|
|
4259
4361
|
this.#settings = next;
|
|
4260
|
-
await
|
|
4261
|
-
await
|
|
4362
|
+
await mkdir8(path14.dirname(this.#storePath), { recursive: true });
|
|
4363
|
+
await writeFile9(this.#storePath, JSON.stringify(next, null, 2), "utf8");
|
|
4262
4364
|
return this.current;
|
|
4263
4365
|
}
|
|
4264
4366
|
};
|
|
4265
|
-
function
|
|
4367
|
+
function sanitize2(value, fallback) {
|
|
4266
4368
|
const result = { ...fallback };
|
|
4267
4369
|
if (typeof value !== "object" || value === null)
|
|
4268
4370
|
return result;
|
|
@@ -4293,10 +4395,13 @@ function sanitize(value, fallback) {
|
|
|
4293
4395
|
if (typeof record.terminalEnabled === "boolean") {
|
|
4294
4396
|
result.terminalEnabled = record.terminalEnabled;
|
|
4295
4397
|
}
|
|
4398
|
+
if (typeof record.liveSessionsFirst === "boolean") {
|
|
4399
|
+
result.liveSessionsFirst = record.liveSessionsFirst;
|
|
4400
|
+
}
|
|
4296
4401
|
return result;
|
|
4297
4402
|
}
|
|
4298
4403
|
function claudeHome() {
|
|
4299
|
-
return
|
|
4404
|
+
return path14.join(homedir4(), ".claude");
|
|
4300
4405
|
}
|
|
4301
4406
|
function writableRoots(mode, cwd, workspacePaths) {
|
|
4302
4407
|
if (mode === "all")
|
|
@@ -4306,7 +4411,7 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
4306
4411
|
return [...roots];
|
|
4307
4412
|
for (const workspace of [cwd, ...workspacePaths]) {
|
|
4308
4413
|
roots.add(workspace);
|
|
4309
|
-
const parent =
|
|
4414
|
+
const parent = path14.dirname(workspace);
|
|
4310
4415
|
if (parent !== workspace)
|
|
4311
4416
|
roots.add(parent);
|
|
4312
4417
|
}
|
|
@@ -4314,9 +4419,9 @@ function writableRoots(mode, cwd, workspacePaths) {
|
|
|
4314
4419
|
}
|
|
4315
4420
|
|
|
4316
4421
|
// ../host/dist/plugins.js
|
|
4317
|
-
import { readFile as
|
|
4318
|
-
import { homedir as
|
|
4319
|
-
import
|
|
4422
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
4423
|
+
import { homedir as homedir5 } from "node:os";
|
|
4424
|
+
import path15 from "node:path";
|
|
4320
4425
|
var TIMEOUT_MS2 = 12e4;
|
|
4321
4426
|
var SAFE_ARG = /^[A-Za-z0-9][A-Za-z0-9._@/-]*$/;
|
|
4322
4427
|
var PluginCli = class {
|
|
@@ -4415,10 +4520,10 @@ async function readCatalog(markets, installed) {
|
|
|
4415
4520
|
return lists.flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
4416
4521
|
}
|
|
4417
4522
|
async function readMarketplace(market, installed) {
|
|
4418
|
-
const root = market.installLocation ||
|
|
4523
|
+
const root = market.installLocation || path15.join(homedir5(), ".claude", "plugins", "marketplaces", market.name);
|
|
4419
4524
|
let manifest;
|
|
4420
4525
|
try {
|
|
4421
|
-
const text = await
|
|
4526
|
+
const text = await readFile12(path15.join(root, ".claude-plugin", "marketplace.json"), "utf8");
|
|
4422
4527
|
manifest = JSON.parse(text);
|
|
4423
4528
|
} catch {
|
|
4424
4529
|
return [];
|
|
@@ -4659,8 +4764,8 @@ function describe4(error) {
|
|
|
4659
4764
|
|
|
4660
4765
|
// ../host/dist/relay-link.js
|
|
4661
4766
|
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
4662
|
-
import { readFile as
|
|
4663
|
-
import
|
|
4767
|
+
import { readFile as readFile13, writeFile as writeFile10, rm as rm3, mkdir as mkdir9 } from "node:fs/promises";
|
|
4768
|
+
import path16 from "node:path";
|
|
4664
4769
|
import { hostname } from "node:os";
|
|
4665
4770
|
import WebSocket from "ws";
|
|
4666
4771
|
|
|
@@ -4715,7 +4820,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4715
4820
|
#log;
|
|
4716
4821
|
constructor(options2) {
|
|
4717
4822
|
super();
|
|
4718
|
-
this.#file =
|
|
4823
|
+
this.#file = path16.join(options2.dataDir, "relay.json");
|
|
4719
4824
|
this.#defaultUrl = options2.defaultUrl.replace(/\/+$/, "");
|
|
4720
4825
|
this.#log = options2.log ?? ((msg) => console.log(`[relay] ${msg}`));
|
|
4721
4826
|
}
|
|
@@ -4796,7 +4901,7 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4796
4901
|
}
|
|
4797
4902
|
async #read() {
|
|
4798
4903
|
try {
|
|
4799
|
-
const raw = await
|
|
4904
|
+
const raw = await readFile13(this.#file, "utf8");
|
|
4800
4905
|
const parsed = JSON.parse(raw);
|
|
4801
4906
|
if (typeof parsed !== "object" || parsed === null)
|
|
4802
4907
|
return null;
|
|
@@ -4809,8 +4914,8 @@ var RelayLink = class extends EventEmitter3 {
|
|
|
4809
4914
|
}
|
|
4810
4915
|
}
|
|
4811
4916
|
async #write(credentials) {
|
|
4812
|
-
await
|
|
4813
|
-
await
|
|
4917
|
+
await mkdir9(path16.dirname(this.#file), { recursive: true });
|
|
4918
|
+
await writeFile10(this.#file, `${JSON.stringify(credentials, null, 2)}
|
|
4814
4919
|
`, {
|
|
4815
4920
|
encoding: "utf8",
|
|
4816
4921
|
mode: 384
|
|
@@ -5067,7 +5172,7 @@ function humanize(error) {
|
|
|
5067
5172
|
// ../host/dist/terminal.js
|
|
5068
5173
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
5069
5174
|
import { access as access2, constants as constants2 } from "node:fs/promises";
|
|
5070
|
-
import
|
|
5175
|
+
import path17 from "node:path";
|
|
5071
5176
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
5072
5177
|
var SCROLLBACK_BYTES = 256 * 1024;
|
|
5073
5178
|
var FLUSH_MS = 12;
|
|
@@ -5237,22 +5342,22 @@ async function defaultShell() {
|
|
|
5237
5342
|
if (pwsh)
|
|
5238
5343
|
return { file: pwsh, args: [], title: "pwsh" };
|
|
5239
5344
|
const system = process.env.SystemRoot ?? "C:\\Windows";
|
|
5240
|
-
const powershell =
|
|
5345
|
+
const powershell = path17.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe");
|
|
5241
5346
|
if (await exists2(powershell))
|
|
5242
5347
|
return { file: powershell, args: [], title: "powershell" };
|
|
5243
5348
|
return { file: process.env.COMSPEC ?? "cmd.exe", args: [], title: "cmd" };
|
|
5244
5349
|
}
|
|
5245
5350
|
const shell = process.env.SHELL;
|
|
5246
5351
|
if (shell && await exists2(shell))
|
|
5247
|
-
return { file: shell, args: [], title:
|
|
5352
|
+
return { file: shell, args: [], title: path17.basename(shell) };
|
|
5248
5353
|
if (await exists2("/bin/bash"))
|
|
5249
5354
|
return { file: "/bin/bash", args: [], title: "bash" };
|
|
5250
5355
|
return { file: "/bin/sh", args: [], title: "sh" };
|
|
5251
5356
|
}
|
|
5252
5357
|
async function findOnPath(name) {
|
|
5253
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
5358
|
+
const dirs = (process.env.PATH ?? "").split(path17.delimiter).filter(Boolean);
|
|
5254
5359
|
for (const dir of dirs) {
|
|
5255
|
-
const candidate =
|
|
5360
|
+
const candidate = path17.join(dir, name);
|
|
5256
5361
|
if (await exists2(candidate))
|
|
5257
5362
|
return candidate;
|
|
5258
5363
|
}
|
|
@@ -5263,10 +5368,10 @@ function exists2(file) {
|
|
|
5263
5368
|
}
|
|
5264
5369
|
|
|
5265
5370
|
// ../host/dist/mcp-config.js
|
|
5266
|
-
import { readFile as
|
|
5267
|
-
import
|
|
5371
|
+
import { readFile as readFile14, writeFile as writeFile11, mkdir as mkdir10 } from "node:fs/promises";
|
|
5372
|
+
import path18 from "node:path";
|
|
5268
5373
|
var MCP_FILE = ".mcp.json";
|
|
5269
|
-
var LOCAL_SETTINGS2 =
|
|
5374
|
+
var LOCAL_SETTINGS2 = path18.join(".claude", "settings.local.json");
|
|
5270
5375
|
var queues = /* @__PURE__ */ new Map();
|
|
5271
5376
|
function serialize(cwd, work) {
|
|
5272
5377
|
const previous = queues.get(cwd) ?? Promise.resolve();
|
|
@@ -5275,7 +5380,7 @@ function serialize(cwd, work) {
|
|
|
5275
5380
|
return next;
|
|
5276
5381
|
}
|
|
5277
5382
|
async function readProjectServers(cwd) {
|
|
5278
|
-
const file = await readJson(
|
|
5383
|
+
const file = await readJson(path18.join(cwd, MCP_FILE));
|
|
5279
5384
|
const servers = file?.mcpServers;
|
|
5280
5385
|
if (!servers || typeof servers !== "object")
|
|
5281
5386
|
return {};
|
|
@@ -5283,23 +5388,23 @@ async function readProjectServers(cwd) {
|
|
|
5283
5388
|
}
|
|
5284
5389
|
function addProjectServer(cwd, name, config) {
|
|
5285
5390
|
return serialize(cwd, async () => {
|
|
5286
|
-
const file = await readJson(
|
|
5391
|
+
const file = await readJson(path18.join(cwd, MCP_FILE)) ?? {};
|
|
5287
5392
|
const servers = file.mcpServers ?? {};
|
|
5288
5393
|
servers[name] = config;
|
|
5289
5394
|
file.mcpServers = servers;
|
|
5290
|
-
await writeJson(
|
|
5395
|
+
await writeJson(path18.join(cwd, MCP_FILE), file);
|
|
5291
5396
|
await approve(cwd, name);
|
|
5292
5397
|
});
|
|
5293
5398
|
}
|
|
5294
5399
|
function removeProjectServer(cwd, name) {
|
|
5295
5400
|
return serialize(cwd, async () => {
|
|
5296
|
-
const file = await readJson(
|
|
5401
|
+
const file = await readJson(path18.join(cwd, MCP_FILE));
|
|
5297
5402
|
const servers = file?.mcpServers;
|
|
5298
5403
|
if (!servers || !(name in servers))
|
|
5299
5404
|
return false;
|
|
5300
5405
|
delete servers[name];
|
|
5301
|
-
await writeJson(
|
|
5302
|
-
const settingsPath =
|
|
5406
|
+
await writeJson(path18.join(cwd, MCP_FILE), file);
|
|
5407
|
+
const settingsPath = path18.join(cwd, LOCAL_SETTINGS2);
|
|
5303
5408
|
const settings = await readJson(settingsPath);
|
|
5304
5409
|
const enabled = settings?.enabledMcpjsonServers;
|
|
5305
5410
|
if (settings && Array.isArray(enabled) && enabled.includes(name)) {
|
|
@@ -5310,7 +5415,7 @@ function removeProjectServer(cwd, name) {
|
|
|
5310
5415
|
});
|
|
5311
5416
|
}
|
|
5312
5417
|
async function approve(cwd, name) {
|
|
5313
|
-
const file =
|
|
5418
|
+
const file = path18.join(cwd, LOCAL_SETTINGS2);
|
|
5314
5419
|
const settings = await readJson(file) ?? {};
|
|
5315
5420
|
const enabled = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
5316
5421
|
if (!enabled.includes(name))
|
|
@@ -5349,21 +5454,21 @@ function describeConfig(config) {
|
|
|
5349
5454
|
}
|
|
5350
5455
|
async function readJson(file) {
|
|
5351
5456
|
try {
|
|
5352
|
-
const raw = await
|
|
5457
|
+
const raw = await readFile14(file, "utf8");
|
|
5353
5458
|
const parsed = JSON.parse(raw);
|
|
5354
5459
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
5355
5460
|
} catch (error) {
|
|
5356
5461
|
if (error.code === "ENOENT")
|
|
5357
5462
|
return null;
|
|
5358
5463
|
if (error instanceof SyntaxError) {
|
|
5359
|
-
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${
|
|
5464
|
+
throw new Error(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C ${path18.basename(file)}: ${error.message}`);
|
|
5360
5465
|
}
|
|
5361
5466
|
throw error;
|
|
5362
5467
|
}
|
|
5363
5468
|
}
|
|
5364
5469
|
async function writeJson(file, data) {
|
|
5365
|
-
await
|
|
5366
|
-
await
|
|
5470
|
+
await mkdir10(path18.dirname(file), { recursive: true });
|
|
5471
|
+
await writeFile11(file, `${JSON.stringify(data, null, 2)}
|
|
5367
5472
|
`, "utf8");
|
|
5368
5473
|
}
|
|
5369
5474
|
|
|
@@ -5377,6 +5482,7 @@ var HostServer = class {
|
|
|
5377
5482
|
#workspaces;
|
|
5378
5483
|
#archive;
|
|
5379
5484
|
#pins;
|
|
5485
|
+
#order;
|
|
5380
5486
|
#folders;
|
|
5381
5487
|
#settings;
|
|
5382
5488
|
#finder;
|
|
@@ -5415,6 +5521,7 @@ var HostServer = class {
|
|
|
5415
5521
|
this.#workspaces = new WorkspaceRegistry(options2.dataDir);
|
|
5416
5522
|
this.#archive = new SessionArchive(options2.dataDir);
|
|
5417
5523
|
this.#pins = new SessionPins(options2.dataDir);
|
|
5524
|
+
this.#order = new ListOrderStore(options2.dataDir);
|
|
5418
5525
|
this.#folders = new Folders(options2.dataDir);
|
|
5419
5526
|
this.#settings = new AppSettingsStore(options2.dataDir);
|
|
5420
5527
|
this.#finder = new ProjectFinder(this.#workspaces);
|
|
@@ -5433,6 +5540,7 @@ var HostServer = class {
|
|
|
5433
5540
|
await this.#workspaces.load();
|
|
5434
5541
|
await this.#archive.load();
|
|
5435
5542
|
await this.#pins.load();
|
|
5543
|
+
await this.#order.load();
|
|
5436
5544
|
await this.#folders.load();
|
|
5437
5545
|
await this.#settings.load();
|
|
5438
5546
|
await this.#github.load();
|
|
@@ -5616,6 +5724,8 @@ var HostServer = class {
|
|
|
5616
5724
|
});
|
|
5617
5725
|
this.#send(socket, { type: "workspace.list", ...this.#workspaceLists() });
|
|
5618
5726
|
this.#sendFolders(socket);
|
|
5727
|
+
this.#send(socket, { type: "order.state", order: this.#order.current });
|
|
5728
|
+
this.#sendLive(socket);
|
|
5619
5729
|
this.#auth.status().then((status) => this.#send(socket, { type: "auth.status", status })).catch(() => void 0);
|
|
5620
5730
|
this.#send(socket, { type: "relay.state", state: this.#relay.state });
|
|
5621
5731
|
return;
|
|
@@ -5731,6 +5841,13 @@ var HostServer = class {
|
|
|
5731
5841
|
case "session.pins":
|
|
5732
5842
|
await this.#sendPinned();
|
|
5733
5843
|
return;
|
|
5844
|
+
case "order.get":
|
|
5845
|
+
this.#send(socket, { type: "order.state", order: this.#order.current });
|
|
5846
|
+
return;
|
|
5847
|
+
case "order.set":
|
|
5848
|
+
await this.#order.set(frame.scope, frame.ids, frame.workspaceId);
|
|
5849
|
+
this.#broadcast({ type: "order.state", order: this.#order.current });
|
|
5850
|
+
return;
|
|
5734
5851
|
case "folder.create": {
|
|
5735
5852
|
if (frame.scope === "session") {
|
|
5736
5853
|
if (!frame.workspaceId)
|
|
@@ -6372,8 +6489,23 @@ var HostServer = class {
|
|
|
6372
6489
|
await session.close(reason);
|
|
6373
6490
|
this.#sessions.delete(sessionId);
|
|
6374
6491
|
this.#broadcast({ type: "session.closed", sessionId, reason });
|
|
6492
|
+
this.#sendLive();
|
|
6375
6493
|
return true;
|
|
6376
6494
|
}
|
|
6495
|
+
/**
|
|
6496
|
+
* Живые сессии всех проектов — вкладке запущенных.
|
|
6497
|
+
*
|
|
6498
|
+
* Без `socket` — всем окнам: состав живых поменялся. Боковые сессии и сессия
|
|
6499
|
+
* поиска проекта в список не идут, их пользователь не заводил.
|
|
6500
|
+
*/
|
|
6501
|
+
#sendLive(socket) {
|
|
6502
|
+
const sessions = [...this.#sessions.values()].filter((s) => s !== this.#finderSession).map((s) => s.summary()).filter((s) => !s.parentSessionId);
|
|
6503
|
+
const frame = { type: "session.live", sessions };
|
|
6504
|
+
if (socket)
|
|
6505
|
+
this.#send(socket, frame);
|
|
6506
|
+
else
|
|
6507
|
+
this.#broadcast(frame);
|
|
6508
|
+
}
|
|
6377
6509
|
/**
|
|
6378
6510
|
* Разослать список сессий проекта всем окнам.
|
|
6379
6511
|
*
|
|
@@ -6435,6 +6567,7 @@ var HostServer = class {
|
|
|
6435
6567
|
this.#wire(session);
|
|
6436
6568
|
this.#sessions.set(session.id, session);
|
|
6437
6569
|
session.start();
|
|
6570
|
+
this.#sendLive();
|
|
6438
6571
|
this.#send(socket, { type: "session.summary", session: session.summary() });
|
|
6439
6572
|
}
|
|
6440
6573
|
/**
|
|
@@ -6548,7 +6681,7 @@ var HostServer = class {
|
|
|
6548
6681
|
*/
|
|
6549
6682
|
#roots() {
|
|
6550
6683
|
const roots = this.#finder.roots();
|
|
6551
|
-
return roots.length > 0 ? roots : [
|
|
6684
|
+
return roots.length > 0 ? roots : [homedir6()];
|
|
6552
6685
|
}
|
|
6553
6686
|
/**
|
|
6554
6687
|
* Где сессии этого каталога разрешено писать без вопроса.
|
|
@@ -6881,15 +7014,15 @@ var HostServer = class {
|
|
|
6881
7014
|
}
|
|
6882
7015
|
#serveStatic(req, res) {
|
|
6883
7016
|
const webRoot = this.#options.webRoot;
|
|
6884
|
-
if (!webRoot || !
|
|
7017
|
+
if (!webRoot || !existsSync4(webRoot)) {
|
|
6885
7018
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
6886
7019
|
res.end("CCV host is running. UI is served by the Vite dev server.\n");
|
|
6887
7020
|
return;
|
|
6888
7021
|
}
|
|
6889
7022
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
6890
|
-
const requested =
|
|
6891
|
-
const target = requested.startsWith(webRoot) &&
|
|
6892
|
-
if (!
|
|
7023
|
+
const requested = path19.join(webRoot, path19.normalize(url2.pathname));
|
|
7024
|
+
const target = requested.startsWith(webRoot) && existsSync4(requested) && statSync3(requested).isFile() ? requested : path19.join(webRoot, "index.html");
|
|
7025
|
+
if (!existsSync4(target)) {
|
|
6893
7026
|
res.writeHead(404).end("not found");
|
|
6894
7027
|
return;
|
|
6895
7028
|
}
|
|
@@ -6914,12 +7047,12 @@ function contentType(file) {
|
|
|
6914
7047
|
".svg": "image/svg+xml",
|
|
6915
7048
|
".woff2": "font/woff2"
|
|
6916
7049
|
};
|
|
6917
|
-
return types[
|
|
7050
|
+
return types[path19.extname(file)] ?? "application/octet-stream";
|
|
6918
7051
|
}
|
|
6919
7052
|
|
|
6920
7053
|
// dist/ccv.js
|
|
6921
|
-
var HERE =
|
|
6922
|
-
var PACKAGE_ROOT =
|
|
7054
|
+
var HERE = path20.dirname(fileURLToPath(import.meta.url));
|
|
7055
|
+
var PACKAGE_ROOT = path20.resolve(HERE, "..");
|
|
6923
7056
|
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.
|
|
6924
7057
|
|
|
6925
7058
|
\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435:
|
|
@@ -6986,7 +7119,7 @@ var server = new HostServer({
|
|
|
6986
7119
|
dataDir: defaults.dataDir,
|
|
6987
7120
|
// UI лежит в пакете рядом с бандлом — это и есть разница между установленной
|
|
6988
7121
|
// утилитой и запуском из репозитория, где сборка живёт в `apps/web/dist`.
|
|
6989
|
-
webRoot: process.env.CCV_WEB_ROOT ??
|
|
7122
|
+
webRoot: process.env.CCV_WEB_ROOT ?? path20.join(PACKAGE_ROOT, "web"),
|
|
6990
7123
|
version,
|
|
6991
7124
|
relayUrl: defaults.relayUrl
|
|
6992
7125
|
});
|
|
@@ -7015,7 +7148,7 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
|
7015
7148
|
}
|
|
7016
7149
|
async function packageVersion() {
|
|
7017
7150
|
try {
|
|
7018
|
-
const text = await
|
|
7151
|
+
const text = await readFile15(path20.join(PACKAGE_ROOT, "package.json"), "utf8");
|
|
7019
7152
|
return JSON.parse(text).version ?? "0.0.0";
|
|
7020
7153
|
} catch {
|
|
7021
7154
|
return "0.0.0";
|