@ao_zorin/zocket 1.2.0 → 1.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/README.md +5 -0
- package/dist/zocket.js +429 -160
- package/docs/INSTALL.md +18 -0
- package/package.json +3 -1
- package/scripts/install-zocket.ps1 +8 -2
- package/scripts/install-zocket.sh +7 -1
package/README.md
CHANGED
|
@@ -41,6 +41,11 @@ zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-por
|
|
|
41
41
|
|
|
42
42
|
Open `http://127.0.0.1:18001`.
|
|
43
43
|
|
|
44
|
+
MCP-only (no web panel):
|
|
45
|
+
```bash
|
|
46
|
+
zocket server --host 127.0.0.1 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
|
|
47
|
+
```
|
|
48
|
+
|
|
44
49
|
## CLI / TUI
|
|
45
50
|
|
|
46
51
|
Full CLI management:
|
package/dist/zocket.js
CHANGED
|
@@ -16,11 +16,26 @@ import { dirname } from "path";
|
|
|
16
16
|
import { lock } from "proper-lockfile";
|
|
17
17
|
|
|
18
18
|
// src/crypto.ts
|
|
19
|
-
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
19
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from "crypto";
|
|
20
20
|
var VERSION = 1;
|
|
21
21
|
var ALGORITHM = "aes-256-gcm";
|
|
22
22
|
var IV_BYTES = 12;
|
|
23
23
|
var TAG_BYTES = 16;
|
|
24
|
+
var FERNET_VERSION = 128;
|
|
25
|
+
function base64UrlToBuffer(raw) {
|
|
26
|
+
const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
27
|
+
const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
28
|
+
return Buffer.from(normalized + pad, "base64");
|
|
29
|
+
}
|
|
30
|
+
function pkcs7Unpad(buf) {
|
|
31
|
+
if (!buf.length) throw new Error("Invalid PKCS7 padding");
|
|
32
|
+
const pad = buf[buf.length - 1];
|
|
33
|
+
if (pad < 1 || pad > 16) throw new Error("Invalid PKCS7 padding");
|
|
34
|
+
for (let i = buf.length - pad; i < buf.length; i += 1) {
|
|
35
|
+
if (buf[i] !== pad) throw new Error("Invalid PKCS7 padding");
|
|
36
|
+
}
|
|
37
|
+
return buf.subarray(0, buf.length - pad);
|
|
38
|
+
}
|
|
24
39
|
function encrypt(plaintext, key) {
|
|
25
40
|
const iv = randomBytes(IV_BYTES);
|
|
26
41
|
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
@@ -40,20 +55,53 @@ function decrypt(data, key) {
|
|
|
40
55
|
decipher.setAuthTag(tag);
|
|
41
56
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
42
57
|
}
|
|
58
|
+
function decryptLegacyFernet(tokenData, keyBase64) {
|
|
59
|
+
const token = tokenData.toString("utf8").trim();
|
|
60
|
+
const raw = base64UrlToBuffer(token);
|
|
61
|
+
if (raw.length < 1 + 8 + 16 + 32) throw new Error("Invalid legacy token length");
|
|
62
|
+
if (raw[0] !== FERNET_VERSION) throw new Error("Invalid legacy token version");
|
|
63
|
+
const key = base64UrlToBuffer(keyBase64);
|
|
64
|
+
if (key.length !== 32) throw new Error("Invalid legacy key length");
|
|
65
|
+
const signingKey = key.subarray(0, 16);
|
|
66
|
+
const encryptionKey = key.subarray(16, 32);
|
|
67
|
+
const hmacStart = raw.length - 32;
|
|
68
|
+
const msg = raw.subarray(0, hmacStart);
|
|
69
|
+
const sig = raw.subarray(hmacStart);
|
|
70
|
+
const mac = createHmac("sha256", signingKey).update(msg).digest();
|
|
71
|
+
if (mac.length !== sig.length || !timingSafeEqual(mac, sig)) {
|
|
72
|
+
throw new Error("Legacy token HMAC verification failed");
|
|
73
|
+
}
|
|
74
|
+
const iv = raw.subarray(1 + 8, 1 + 8 + 16);
|
|
75
|
+
const ciphertext = raw.subarray(1 + 8 + 16, hmacStart);
|
|
76
|
+
const decipher = createDecipheriv("aes-128-cbc", encryptionKey, iv);
|
|
77
|
+
decipher.setAutoPadding(false);
|
|
78
|
+
const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
79
|
+
return pkcs7Unpad(padded);
|
|
80
|
+
}
|
|
43
81
|
|
|
44
82
|
// src/vault.ts
|
|
45
83
|
var PROJECT_RE = /^[a-zA-Z0-9._-]+$/;
|
|
46
84
|
var SECRET_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
47
85
|
var VaultService = class {
|
|
48
|
-
constructor(vaultPath2, lockFile, key) {
|
|
86
|
+
constructor(vaultPath2, lockFile, key, legacyKeyBase64, keyFilePath) {
|
|
49
87
|
this.vaultPath = vaultPath2;
|
|
50
88
|
this.lockFile = lockFile;
|
|
51
89
|
this.key = key;
|
|
90
|
+
this.legacyKeyBase64 = legacyKeyBase64;
|
|
91
|
+
this.keyFilePath = keyFilePath;
|
|
52
92
|
}
|
|
53
93
|
load() {
|
|
54
94
|
if (!existsSync(this.vaultPath)) return { version: 1, projects: {} };
|
|
55
95
|
const raw = readFileSync(this.vaultPath);
|
|
56
|
-
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(decrypt(raw, this.key).toString("utf8"));
|
|
98
|
+
} catch (err2) {
|
|
99
|
+
const message = String(err2);
|
|
100
|
+
if (this.legacyKeyBase64 && message.includes("Unsupported vault version")) {
|
|
101
|
+
return this.migrateLegacy(raw);
|
|
102
|
+
}
|
|
103
|
+
throw err2;
|
|
104
|
+
}
|
|
57
105
|
}
|
|
58
106
|
save(data) {
|
|
59
107
|
mkdirSync(dirname(this.vaultPath), { recursive: true });
|
|
@@ -64,6 +112,29 @@ var VaultService = class {
|
|
|
64
112
|
this.save({ version: 1, projects: {} });
|
|
65
113
|
}
|
|
66
114
|
}
|
|
115
|
+
migrateLegacy(raw) {
|
|
116
|
+
const plaintext = decryptLegacyFernet(raw, this.legacyKeyBase64);
|
|
117
|
+
let parsed;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(plaintext.toString("utf8"));
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error("Legacy vault JSON parse failed");
|
|
122
|
+
}
|
|
123
|
+
const data = {
|
|
124
|
+
version: typeof parsed.version === "number" ? parsed.version : 1,
|
|
125
|
+
projects: parsed.projects && typeof parsed.projects === "object" ? parsed.projects : {}
|
|
126
|
+
};
|
|
127
|
+
if (!data.version || data.version !== 1) data.version = 1;
|
|
128
|
+
if (!data.projects) data.projects = {};
|
|
129
|
+
this.save(data);
|
|
130
|
+
if (this.keyFilePath) {
|
|
131
|
+
try {
|
|
132
|
+
writeFileSync(this.keyFilePath, this.key.toString("hex"), { mode: 384 });
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
67
138
|
async withLock(fn) {
|
|
68
139
|
mkdirSync(dirname(this.lockFile), { recursive: true });
|
|
69
140
|
if (!existsSync(this.lockFile)) writeFileSync(this.lockFile, "");
|
|
@@ -1117,12 +1188,12 @@ function createMcpServer(services, options = {}) {
|
|
|
1117
1188
|
// src/web.ts
|
|
1118
1189
|
import { Hono } from "hono";
|
|
1119
1190
|
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
|
|
1120
|
-
import { createHmac, randomBytes as randomBytes5 } from "crypto";
|
|
1191
|
+
import { createHmac as createHmac2, randomBytes as randomBytes5 } from "crypto";
|
|
1121
1192
|
import { readdirSync, statSync, existsSync as existsSync4 } from "fs";
|
|
1122
1193
|
import { join as join2, resolve as resolvePath, dirname as dirname4 } from "path";
|
|
1123
1194
|
|
|
1124
1195
|
// src/auth.ts
|
|
1125
|
-
import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual } from "crypto";
|
|
1196
|
+
import { randomBytes as randomBytes4, pbkdf2Sync, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1126
1197
|
var ITERATIONS = 6e5;
|
|
1127
1198
|
var KEY_LEN = 32;
|
|
1128
1199
|
var DIGEST = "sha256";
|
|
@@ -1135,7 +1206,7 @@ function verifyPassword(password, hash, salt) {
|
|
|
1135
1206
|
const derived = pbkdf2Sync(password, salt, ITERATIONS, KEY_LEN, DIGEST);
|
|
1136
1207
|
const expected = Buffer.from(hash, "hex");
|
|
1137
1208
|
if (derived.length !== expected.length) return false;
|
|
1138
|
-
return
|
|
1209
|
+
return timingSafeEqual2(derived, expected);
|
|
1139
1210
|
}
|
|
1140
1211
|
|
|
1141
1212
|
// src/i18n.ts
|
|
@@ -1337,7 +1408,7 @@ function normalizeLang(lang) {
|
|
|
1337
1408
|
var COOKIE = "zs";
|
|
1338
1409
|
function signSession(data, secret) {
|
|
1339
1410
|
const payload = Buffer.from(JSON.stringify(data)).toString("base64url");
|
|
1340
|
-
const sig =
|
|
1411
|
+
const sig = createHmac2("sha256", secret).update(payload).digest("hex").slice(0, 20);
|
|
1341
1412
|
return `${payload}.${sig}`;
|
|
1342
1413
|
}
|
|
1343
1414
|
function parseSession(value, secret) {
|
|
@@ -1345,7 +1416,7 @@ function parseSession(value, secret) {
|
|
|
1345
1416
|
if (dot === -1) return null;
|
|
1346
1417
|
const payload = value.slice(0, dot);
|
|
1347
1418
|
const sig = value.slice(dot + 1);
|
|
1348
|
-
const expected =
|
|
1419
|
+
const expected = createHmac2("sha256", secret).update(payload).digest("hex").slice(0, 20);
|
|
1349
1420
|
if (sig !== expected) return null;
|
|
1350
1421
|
try {
|
|
1351
1422
|
return JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
@@ -1925,177 +1996,360 @@ function lockPath(home = zocketHome()) {
|
|
|
1925
1996
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
1926
1997
|
import { dirname as dirname5 } from "path";
|
|
1927
1998
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
1999
|
+
function isHexKey(raw) {
|
|
2000
|
+
return /^[0-9a-fA-F]{64}$/.test(raw);
|
|
2001
|
+
}
|
|
2002
|
+
function isBase64UrlKey(raw) {
|
|
2003
|
+
return /^[A-Za-z0-9_-]{43,44}=?$/.test(raw);
|
|
2004
|
+
}
|
|
2005
|
+
function base64UrlToBuffer2(raw) {
|
|
2006
|
+
const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
2007
|
+
const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
2008
|
+
return Buffer.from(normalized + pad, "base64");
|
|
2009
|
+
}
|
|
1928
2010
|
function loadOrCreateKey(keyFile) {
|
|
1929
2011
|
if (existsSync5(keyFile)) {
|
|
1930
2012
|
const raw = readFileSync5(keyFile, "utf8").trim();
|
|
1931
|
-
|
|
2013
|
+
if (isHexKey(raw)) {
|
|
2014
|
+
return { key: Buffer.from(raw, "hex"), source: "hex" };
|
|
2015
|
+
}
|
|
2016
|
+
if (isBase64UrlKey(raw)) {
|
|
2017
|
+
const key2 = base64UrlToBuffer2(raw);
|
|
2018
|
+
if (key2.length !== 32) {
|
|
2019
|
+
throw new Error("Legacy base64 key is invalid length");
|
|
2020
|
+
}
|
|
2021
|
+
return { key: key2, source: "base64", legacyBase64: raw };
|
|
2022
|
+
}
|
|
2023
|
+
throw new Error("Unsupported key format: expected 64-char hex or base64url");
|
|
1932
2024
|
}
|
|
1933
2025
|
mkdirSync4(dirname5(keyFile), { recursive: true });
|
|
1934
2026
|
const key = randomBytes6(32);
|
|
1935
2027
|
writeFileSync5(keyFile, key.toString("hex"), { mode: 384 });
|
|
1936
|
-
return key;
|
|
2028
|
+
return { key, source: "generated" };
|
|
1937
2029
|
}
|
|
1938
2030
|
|
|
1939
2031
|
// src/tui.ts
|
|
1940
|
-
import
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
${title}
|
|
1945
|
-
`);
|
|
1946
|
-
options.forEach((opt, i) => output.write(` ${i + 1}) ${opt}
|
|
1947
|
-
`));
|
|
1948
|
-
const answer = await rl.question("> ");
|
|
1949
|
-
const idx = Number(answer.trim()) - 1;
|
|
1950
|
-
return Number.isFinite(idx) && idx >= 0 && idx < options.length ? idx : -1;
|
|
1951
|
-
}
|
|
1952
|
-
async function promptInput(rl, label, fallback = "") {
|
|
1953
|
-
const ans = await rl.question(`${label}${fallback ? ` (${fallback})` : ""}: `);
|
|
1954
|
-
return ans.trim() || fallback;
|
|
1955
|
-
}
|
|
1956
|
-
async function promptConfirm(rl, label) {
|
|
1957
|
-
const ans = await rl.question(`${label} [y/N]: `);
|
|
1958
|
-
return ans.trim().toLowerCase() === "y";
|
|
2032
|
+
import blessed from "blessed";
|
|
2033
|
+
function safeText(value) {
|
|
2034
|
+
if (!value) return "";
|
|
2035
|
+
return String(value);
|
|
1959
2036
|
}
|
|
1960
|
-
|
|
1961
|
-
const
|
|
1962
|
-
|
|
1963
|
-
output.write("No projects\n");
|
|
1964
|
-
return null;
|
|
1965
|
-
}
|
|
1966
|
-
const idx = await promptMenu(rl, "Select project", rows.map((r) => `${r.name} (${r.secret_count})`));
|
|
1967
|
-
if (idx < 0) return null;
|
|
1968
|
-
return rows[idx].name;
|
|
2037
|
+
function buildProjectLabel(row) {
|
|
2038
|
+
const count = row.secret_count ?? 0;
|
|
2039
|
+
return `${row.name} (${count})`;
|
|
1969
2040
|
}
|
|
1970
|
-
async function
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
rows.forEach((r) => {
|
|
1977
|
-
output.write(`${r.name} ${r.secret_count} ${r.folder_path ?? ""}
|
|
1978
|
-
`);
|
|
2041
|
+
async function runTui(services) {
|
|
2042
|
+
const { vault } = services;
|
|
2043
|
+
const screen = blessed.screen({
|
|
2044
|
+
smartCSR: true,
|
|
2045
|
+
title: "zocket"
|
|
1979
2046
|
});
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2047
|
+
const header = blessed.box({
|
|
2048
|
+
parent: screen,
|
|
2049
|
+
top: 0,
|
|
2050
|
+
height: 1,
|
|
2051
|
+
width: "100%",
|
|
2052
|
+
style: { fg: "white", bg: "blue" },
|
|
2053
|
+
content: " zocket \u2022 TUI | q:quit tab:switch r:refresh v:values n:new project d:delete s:set secret e:edit x:delete"
|
|
2054
|
+
});
|
|
2055
|
+
const footer = blessed.box({
|
|
2056
|
+
parent: screen,
|
|
2057
|
+
bottom: 0,
|
|
2058
|
+
height: 1,
|
|
2059
|
+
width: "100%",
|
|
2060
|
+
style: { fg: "black", bg: "white" },
|
|
2061
|
+
content: " Ready"
|
|
2062
|
+
});
|
|
2063
|
+
const projectList = blessed.list({
|
|
2064
|
+
parent: screen,
|
|
2065
|
+
label: " Projects ",
|
|
2066
|
+
top: 1,
|
|
2067
|
+
left: 0,
|
|
2068
|
+
bottom: 1,
|
|
2069
|
+
width: "30%",
|
|
2070
|
+
border: "line",
|
|
2071
|
+
keys: true,
|
|
2072
|
+
mouse: true,
|
|
2073
|
+
style: {
|
|
2074
|
+
selected: { bg: "blue", fg: "white" },
|
|
2075
|
+
border: { fg: "cyan" }
|
|
2076
|
+
},
|
|
2077
|
+
scrollbar: {
|
|
2078
|
+
ch: " ",
|
|
2079
|
+
inverse: true
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
const secretTable = blessed.listtable({
|
|
2083
|
+
parent: screen,
|
|
2084
|
+
label: " Secrets ",
|
|
2085
|
+
top: 1,
|
|
2086
|
+
left: "30%",
|
|
2087
|
+
bottom: 1,
|
|
2088
|
+
width: "70%",
|
|
2089
|
+
border: "line",
|
|
2090
|
+
keys: true,
|
|
2091
|
+
mouse: true,
|
|
2092
|
+
align: "left",
|
|
2093
|
+
noCellBorders: true,
|
|
2094
|
+
style: {
|
|
2095
|
+
header: { fg: "yellow", bold: true },
|
|
2096
|
+
cell: { fg: "white" },
|
|
2097
|
+
selected: { bg: "blue", fg: "white" },
|
|
2098
|
+
border: { fg: "cyan" }
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
const prompt = blessed.prompt({
|
|
2102
|
+
parent: screen,
|
|
2103
|
+
border: "line",
|
|
2104
|
+
height: 9,
|
|
2105
|
+
width: "60%",
|
|
2106
|
+
top: "center",
|
|
2107
|
+
left: "center",
|
|
2108
|
+
label: " Input ",
|
|
2109
|
+
keys: true,
|
|
2110
|
+
vi: true
|
|
2111
|
+
});
|
|
2112
|
+
const confirm = blessed.question({
|
|
2113
|
+
parent: screen,
|
|
2114
|
+
border: "line",
|
|
2115
|
+
height: 7,
|
|
2116
|
+
width: "60%",
|
|
2117
|
+
top: "center",
|
|
2118
|
+
left: "center",
|
|
2119
|
+
label: " Confirm ",
|
|
2120
|
+
keys: true,
|
|
2121
|
+
vi: true
|
|
2122
|
+
});
|
|
2123
|
+
let projects = [];
|
|
2124
|
+
let secrets = [];
|
|
2125
|
+
let currentProject = null;
|
|
2126
|
+
let showValues = false;
|
|
2127
|
+
function setStatus(message) {
|
|
2128
|
+
footer.setContent(` ${message}`);
|
|
2129
|
+
screen.render();
|
|
2130
|
+
}
|
|
2131
|
+
function setFocusPane(pane) {
|
|
2132
|
+
if (pane === "projects") {
|
|
2133
|
+
projectList.style.border.fg = "green";
|
|
2134
|
+
secretTable.style.border.fg = "cyan";
|
|
2135
|
+
projectList.setLabel(" Projects ");
|
|
2136
|
+
secretTable.setLabel(" Secrets ");
|
|
2137
|
+
projectList.focus();
|
|
1992
2138
|
} else {
|
|
1993
|
-
|
|
1994
|
-
|
|
2139
|
+
projectList.style.border.fg = "cyan";
|
|
2140
|
+
secretTable.style.border.fg = "green";
|
|
2141
|
+
projectList.setLabel(" Projects ");
|
|
2142
|
+
secretTable.setLabel(" Secrets ");
|
|
2143
|
+
secretTable.focus();
|
|
1995
2144
|
}
|
|
2145
|
+
screen.render();
|
|
1996
2146
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
"
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
"Delete",
|
|
2061
|
-
"Back"
|
|
2062
|
-
]);
|
|
2063
|
-
if (idx === 5) continue;
|
|
2064
|
-
if (idx === 0) await showSecrets(vault, project, false);
|
|
2065
|
-
if (idx === 1) await showSecrets(vault, project, true);
|
|
2066
|
-
if (idx === 2) {
|
|
2067
|
-
const key = await promptInput(rl, "Key (UPPERCASE)", "");
|
|
2068
|
-
const value = await promptInput(rl, "Value", "");
|
|
2069
|
-
const desc = await promptInput(rl, "Description", "");
|
|
2070
|
-
await vault.setSecret(project, key, value, desc);
|
|
2071
|
-
output.write("Secret saved\n");
|
|
2072
|
-
}
|
|
2073
|
-
if (idx === 3) {
|
|
2074
|
-
const key = await promptInput(rl, "Key", "");
|
|
2075
|
-
const value = await vault.getSecretValue(project, key);
|
|
2076
|
-
output.write(`${value}
|
|
2077
|
-
`);
|
|
2078
|
-
}
|
|
2079
|
-
if (idx === 4) {
|
|
2080
|
-
const key = await promptInput(rl, "Key", "");
|
|
2081
|
-
if (await promptConfirm(rl, `Delete ${key}?`)) {
|
|
2082
|
-
await vault.deleteSecret(project, key);
|
|
2083
|
-
output.write("Secret deleted\n");
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2147
|
+
async function askInput(label, initial = "") {
|
|
2148
|
+
return await new Promise((resolve) => {
|
|
2149
|
+
prompt.input(label, initial, (err2, value) => {
|
|
2150
|
+
if (err2) return resolve(null);
|
|
2151
|
+
resolve(value ?? "");
|
|
2152
|
+
});
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
async function askConfirm(label) {
|
|
2156
|
+
return await new Promise((resolve) => {
|
|
2157
|
+
confirm.ask(label, (err2, answer) => {
|
|
2158
|
+
if (err2) return resolve(false);
|
|
2159
|
+
resolve(Boolean(answer));
|
|
2160
|
+
});
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
async function refreshProjects(selectName) {
|
|
2164
|
+
projects = await vault.listProjects();
|
|
2165
|
+
if (!projects.length) {
|
|
2166
|
+
projectList.setItems(["(no projects)"]);
|
|
2167
|
+
projectList.select(0);
|
|
2168
|
+
currentProject = null;
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const labels = projects.map(buildProjectLabel);
|
|
2172
|
+
projectList.setItems(labels);
|
|
2173
|
+
let idx = 0;
|
|
2174
|
+
if (selectName) {
|
|
2175
|
+
const found = projects.findIndex((p) => p.name === selectName);
|
|
2176
|
+
if (found >= 0) idx = found;
|
|
2177
|
+
} else if (currentProject) {
|
|
2178
|
+
const found = projects.findIndex((p) => p.name === currentProject?.name);
|
|
2179
|
+
if (found >= 0) idx = found;
|
|
2180
|
+
}
|
|
2181
|
+
projectList.select(idx);
|
|
2182
|
+
currentProject = projects[idx];
|
|
2183
|
+
}
|
|
2184
|
+
async function refreshSecrets() {
|
|
2185
|
+
if (!currentProject) {
|
|
2186
|
+
secretTable.setData([["Key", "Value", "Description", "Updated"]]);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
secrets = await vault.listSecrets(currentProject.name);
|
|
2190
|
+
let rows;
|
|
2191
|
+
if (!secrets.length) {
|
|
2192
|
+
rows = [[showValues ? "Key" : "Key", showValues ? "Value" : "Description", "Updated"]];
|
|
2193
|
+
} else {
|
|
2194
|
+
if (showValues) {
|
|
2195
|
+
const values = await Promise.all(secrets.map((s) => vault.getSecretValue(currentProject.name, s.key)));
|
|
2196
|
+
rows = [
|
|
2197
|
+
["Key", "Value", "Description", "Updated"],
|
|
2198
|
+
...secrets.map((s, i) => [
|
|
2199
|
+
s.key,
|
|
2200
|
+
safeText(values[i]),
|
|
2201
|
+
safeText(s.description),
|
|
2202
|
+
safeText(s.updated_at)
|
|
2203
|
+
])
|
|
2204
|
+
];
|
|
2205
|
+
} else {
|
|
2206
|
+
rows = [
|
|
2207
|
+
["Key", "Description", "Updated"],
|
|
2208
|
+
...secrets.map((s) => [s.key, safeText(s.description), safeText(s.updated_at)])
|
|
2209
|
+
];
|
|
2086
2210
|
}
|
|
2087
2211
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2212
|
+
secretTable.setData(rows);
|
|
2213
|
+
}
|
|
2214
|
+
async function refreshAll() {
|
|
2215
|
+
await refreshProjects();
|
|
2216
|
+
await refreshSecrets();
|
|
2217
|
+
screen.render();
|
|
2218
|
+
}
|
|
2219
|
+
async function createProject() {
|
|
2220
|
+
const name = await askInput("Project name");
|
|
2221
|
+
if (!name) return;
|
|
2222
|
+
const desc = await askInput("Description (optional)", "");
|
|
2223
|
+
const folder = await askInput("Folder path (optional)", "");
|
|
2224
|
+
await vault.createProject(name, desc ?? "");
|
|
2225
|
+
if (folder) await vault.setFolder(name, folder);
|
|
2226
|
+
setStatus(`Project created: ${name}`);
|
|
2227
|
+
await refreshProjects(name);
|
|
2228
|
+
await refreshSecrets();
|
|
2229
|
+
}
|
|
2230
|
+
async function deleteProject() {
|
|
2231
|
+
if (!currentProject) return;
|
|
2232
|
+
const ok2 = await askConfirm(`Delete project ${currentProject.name}?`);
|
|
2233
|
+
if (!ok2) return;
|
|
2234
|
+
await vault.deleteProject(currentProject.name);
|
|
2235
|
+
setStatus(`Project deleted: ${currentProject.name}`);
|
|
2236
|
+
await refreshProjects();
|
|
2237
|
+
await refreshSecrets();
|
|
2238
|
+
}
|
|
2239
|
+
async function setProjectFolder() {
|
|
2240
|
+
if (!currentProject) return;
|
|
2241
|
+
const path = await askInput("Folder path (empty to clear)", currentProject.folder_path ?? "");
|
|
2242
|
+
if (path === null) return;
|
|
2243
|
+
await vault.setFolder(currentProject.name, path.trim() || void 0);
|
|
2244
|
+
setStatus("Folder updated");
|
|
2245
|
+
await refreshProjects(currentProject.name);
|
|
2246
|
+
}
|
|
2247
|
+
async function setAllowedDomains() {
|
|
2248
|
+
if (!currentProject) return;
|
|
2249
|
+
const initial = (await vault.getAllowedDomains(currentProject.name))?.join(", ") ?? "";
|
|
2250
|
+
const domains = await askInput("Allowed domains (comma-separated)", initial);
|
|
2251
|
+
if (domains === null) return;
|
|
2252
|
+
const value = domains.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2253
|
+
await vault.setAllowedDomains(currentProject.name, value.length ? value : null);
|
|
2254
|
+
setStatus("Allowed domains updated");
|
|
2255
|
+
}
|
|
2256
|
+
function selectedSecretKey() {
|
|
2257
|
+
if (!secrets.length) return null;
|
|
2258
|
+
const idx = secretTable.selected - 1;
|
|
2259
|
+
if (idx < 0 || idx >= secrets.length) return null;
|
|
2260
|
+
return secrets[idx].key;
|
|
2090
2261
|
}
|
|
2262
|
+
async function upsertSecret(edit = false) {
|
|
2263
|
+
if (!currentProject) return;
|
|
2264
|
+
let key = "";
|
|
2265
|
+
let value = "";
|
|
2266
|
+
let desc = "";
|
|
2267
|
+
if (edit) {
|
|
2268
|
+
const selectedKey = selectedSecretKey();
|
|
2269
|
+
if (!selectedKey) return;
|
|
2270
|
+
key = selectedKey;
|
|
2271
|
+
desc = secrets.find((s) => s.key === selectedKey)?.description ?? "";
|
|
2272
|
+
value = showValues ? await vault.getSecretValue(currentProject.name, selectedKey) : "";
|
|
2273
|
+
}
|
|
2274
|
+
const keyInput = await askInput("Key (UPPERCASE)", key);
|
|
2275
|
+
if (!keyInput) return;
|
|
2276
|
+
const valueInput = await askInput("Value", value);
|
|
2277
|
+
if (valueInput === null) return;
|
|
2278
|
+
const descInput = await askInput("Description (optional)", desc);
|
|
2279
|
+
await vault.setSecret(currentProject.name, keyInput, valueInput, descInput ?? "");
|
|
2280
|
+
setStatus("Secret saved");
|
|
2281
|
+
await refreshSecrets();
|
|
2282
|
+
}
|
|
2283
|
+
async function deleteSecret() {
|
|
2284
|
+
if (!currentProject) return;
|
|
2285
|
+
const key = selectedSecretKey();
|
|
2286
|
+
if (!key) return;
|
|
2287
|
+
const ok2 = await askConfirm(`Delete secret ${key}?`);
|
|
2288
|
+
if (!ok2) return;
|
|
2289
|
+
await vault.deleteSecret(currentProject.name, key);
|
|
2290
|
+
setStatus(`Secret deleted: ${key}`);
|
|
2291
|
+
await refreshSecrets();
|
|
2292
|
+
}
|
|
2293
|
+
projectList.on("select", async (_, idx) => {
|
|
2294
|
+
currentProject = projects[idx] ?? null;
|
|
2295
|
+
await refreshSecrets();
|
|
2296
|
+
screen.render();
|
|
2297
|
+
});
|
|
2298
|
+
screen.key(["q", "C-c"], () => process.exit(0));
|
|
2299
|
+
screen.key(["tab"], () => {
|
|
2300
|
+
if (screen.focused === projectList) setFocusPane("secrets");
|
|
2301
|
+
else setFocusPane("projects");
|
|
2302
|
+
});
|
|
2303
|
+
screen.key(["r"], async () => {
|
|
2304
|
+
setStatus("Refreshing...");
|
|
2305
|
+
await refreshAll();
|
|
2306
|
+
setStatus("Ready");
|
|
2307
|
+
});
|
|
2308
|
+
screen.key(["v"], async () => {
|
|
2309
|
+
showValues = !showValues;
|
|
2310
|
+
setStatus(showValues ? "Values: visible" : "Values: hidden");
|
|
2311
|
+
await refreshSecrets();
|
|
2312
|
+
screen.render();
|
|
2313
|
+
});
|
|
2314
|
+
screen.key(["n"], async () => {
|
|
2315
|
+
await createProject();
|
|
2316
|
+
screen.render();
|
|
2317
|
+
});
|
|
2318
|
+
screen.key(["d"], async () => {
|
|
2319
|
+
await deleteProject();
|
|
2320
|
+
screen.render();
|
|
2321
|
+
});
|
|
2322
|
+
screen.key(["f"], async () => {
|
|
2323
|
+
await setProjectFolder();
|
|
2324
|
+
screen.render();
|
|
2325
|
+
});
|
|
2326
|
+
screen.key(["a"], async () => {
|
|
2327
|
+
await setAllowedDomains();
|
|
2328
|
+
screen.render();
|
|
2329
|
+
});
|
|
2330
|
+
screen.key(["s"], async () => {
|
|
2331
|
+
await upsertSecret(false);
|
|
2332
|
+
screen.render();
|
|
2333
|
+
});
|
|
2334
|
+
screen.key(["e"], async () => {
|
|
2335
|
+
await upsertSecret(true);
|
|
2336
|
+
screen.render();
|
|
2337
|
+
});
|
|
2338
|
+
screen.key(["x"], async () => {
|
|
2339
|
+
await deleteSecret();
|
|
2340
|
+
screen.render();
|
|
2341
|
+
});
|
|
2342
|
+
await refreshAll();
|
|
2343
|
+
setFocusPane("projects");
|
|
2344
|
+
screen.render();
|
|
2091
2345
|
}
|
|
2092
2346
|
|
|
2093
2347
|
// src/cli.ts
|
|
2094
2348
|
function createServices() {
|
|
2095
2349
|
const home = zocketHome();
|
|
2096
2350
|
mkdirSync5(home, { recursive: true });
|
|
2097
|
-
const
|
|
2098
|
-
const vault = new VaultService(vaultPath(), lockPath(), key);
|
|
2351
|
+
const keyMaterial = loadOrCreateKey(keyPath());
|
|
2352
|
+
const vault = new VaultService(vaultPath(), lockPath(), keyMaterial.key, keyMaterial.legacyBase64, keyPath());
|
|
2099
2353
|
vault.ensureExists();
|
|
2100
2354
|
const config = new ConfigStore(configPath());
|
|
2101
2355
|
const audit = new AuditLogger(auditPath());
|
|
@@ -2107,10 +2361,14 @@ async function cmdStart(opts) {
|
|
|
2107
2361
|
const cfg = config.ensureExists();
|
|
2108
2362
|
const mode = opts.mode === "admin" ? "admin" : "metadata";
|
|
2109
2363
|
const services = { vault, config, audit, mode };
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2364
|
+
if (opts.webEnabled) {
|
|
2365
|
+
const webApp = createWebApp({ vault, config, audit });
|
|
2366
|
+
serve({ fetch: webApp.fetch, hostname: opts.host, port: opts.webPort }, () => {
|
|
2367
|
+
console.log(`[zocket] web http://${opts.host}:${opts.webPort}`);
|
|
2368
|
+
});
|
|
2369
|
+
} else {
|
|
2370
|
+
console.log("[zocket] web disabled");
|
|
2371
|
+
}
|
|
2114
2372
|
const sessions = /* @__PURE__ */ new Map();
|
|
2115
2373
|
const mcpHttp = createServer((req, res) => {
|
|
2116
2374
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -2223,13 +2481,24 @@ function buildCli() {
|
|
|
2223
2481
|
createServices();
|
|
2224
2482
|
console.log(`[zocket] initialized \u2014 vault: ${vaultPath()}`);
|
|
2225
2483
|
});
|
|
2226
|
-
program.command("start").description("Start web panel + MCP SSE + MCP Streamable HTTP servers").option("--host <host>", "Bind host", "127.0.0.1").option("--web-port <port>", "Web panel port", "18001").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mcp-stream-port <port>", "MCP Streamable HTTP port", "18003").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").action(async (opts) => {
|
|
2484
|
+
program.command("start").description("Start web panel + MCP SSE + MCP Streamable HTTP servers (use --no-web for MCP-only)").option("--host <host>", "Bind host", "127.0.0.1").option("--web-port <port>", "Web panel port", "18001").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mcp-stream-port <port>", "MCP Streamable HTTP port", "18003").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").option("--no-web", "Disable web panel").action(async (opts) => {
|
|
2227
2485
|
await cmdStart({
|
|
2228
2486
|
host: opts.host,
|
|
2229
2487
|
webPort: parseInt(opts.webPort, 10),
|
|
2230
2488
|
mcpPort: parseInt(opts.mcpPort, 10),
|
|
2231
2489
|
mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
|
|
2232
|
-
mode: opts.mode
|
|
2490
|
+
mode: opts.mode,
|
|
2491
|
+
webEnabled: opts.web
|
|
2492
|
+
});
|
|
2493
|
+
});
|
|
2494
|
+
program.command("server").description("Start MCP servers without web panel").option("--host <host>", "Bind host", "127.0.0.1").option("--mcp-port <port>", "MCP SSE port", "18002").option("--mcp-stream-port <port>", "MCP Streamable HTTP port", "18003").option("--mode <mode>", "MCP mode (metadata|admin)", "admin").action(async (opts) => {
|
|
2495
|
+
await cmdStart({
|
|
2496
|
+
host: opts.host,
|
|
2497
|
+
webPort: 18001,
|
|
2498
|
+
mcpPort: parseInt(opts.mcpPort, 10),
|
|
2499
|
+
mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
|
|
2500
|
+
mode: opts.mode,
|
|
2501
|
+
webEnabled: false
|
|
2233
2502
|
});
|
|
2234
2503
|
});
|
|
2235
2504
|
const projects = program.command("projects").description("Manage projects");
|
package/docs/INSTALL.md
CHANGED
|
@@ -12,6 +12,11 @@ This guide installs **zocket** (Node.js) as:
|
|
|
12
12
|
curl -fsSL https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.sh | bash
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
MCP-only (no web panel):
|
|
16
|
+
```bash
|
|
17
|
+
curl -fsSL https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.sh | bash -s -- --no-web
|
|
18
|
+
```
|
|
19
|
+
|
|
15
20
|
If you run from a local clone:
|
|
16
21
|
```bash
|
|
17
22
|
bash scripts/install-zocket.sh --source local
|
|
@@ -22,6 +27,13 @@ bash scripts/install-zocket.sh --source local
|
|
|
22
27
|
irm https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.ps1 | iex
|
|
23
28
|
```
|
|
24
29
|
|
|
30
|
+
MCP-only (no web panel):
|
|
31
|
+
```powershell
|
|
32
|
+
$tmp = "$env:TEMP\\install-zocket.ps1"
|
|
33
|
+
irm https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.ps1 -OutFile $tmp
|
|
34
|
+
powershell -ExecutionPolicy Bypass -File $tmp -NoWeb
|
|
35
|
+
```
|
|
36
|
+
|
|
25
37
|
If you run from a local clone:
|
|
26
38
|
```powershell
|
|
27
39
|
powershell -ExecutionPolicy Bypass -File .\scripts\install-zocket.ps1 -Source Local
|
|
@@ -93,6 +105,11 @@ zocket init
|
|
|
93
105
|
zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
|
|
94
106
|
```
|
|
95
107
|
|
|
108
|
+
MCP-only (no web panel):
|
|
109
|
+
```bash
|
|
110
|
+
zocket server --host 127.0.0.1 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
|
|
111
|
+
```
|
|
112
|
+
|
|
96
113
|
## 6) Systemd hardening on Linux (production)
|
|
97
114
|
|
|
98
115
|
If you install with `--autostart system`, the installer creates and enables:
|
|
@@ -155,6 +172,7 @@ Or create manually:
|
|
|
155
172
|
- task `Zocket` on logon
|
|
156
173
|
- action:
|
|
157
174
|
- `zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin`
|
|
175
|
+
- add `--no-web` for MCP-only
|
|
158
176
|
|
|
159
177
|
## 7) First web open
|
|
160
178
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ao_zorin/zocket",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Local encrypted vault + web panel + MCP server for AI agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
19
19
|
"@hono/node-server": "^1.13.0",
|
|
20
20
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
21
|
+
"blessed": "^0.1.81",
|
|
21
22
|
"commander": "^12.0.0",
|
|
22
23
|
"hono": "^4.0.0",
|
|
23
24
|
"proper-lockfile": "^4.1.2",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
37
|
+
"@types/blessed": "^0.1.24",
|
|
36
38
|
"@types/node": "^20.0.0",
|
|
37
39
|
"@types/proper-lockfile": "^4.1.4",
|
|
38
40
|
"js-tiktoken": "^1.0.21",
|
|
@@ -11,7 +11,8 @@ Param(
|
|
|
11
11
|
[int]$McpStreamPort = 18003,
|
|
12
12
|
[ValidateSet("metadata", "admin")]
|
|
13
13
|
[string]$McpMode = "admin",
|
|
14
|
-
[bool]$EnableAutostart = $true
|
|
14
|
+
[bool]$EnableAutostart = $true,
|
|
15
|
+
[switch]$NoWeb
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
$ErrorActionPreference = "Stop"
|
|
@@ -63,12 +64,17 @@ $env:ZOCKET_HOME = $ZocketHome
|
|
|
63
64
|
if ($EnableAutostart) {
|
|
64
65
|
$taskName = "Zocket"
|
|
65
66
|
$cmd = "\"$zocketBin\" start --host 127.0.0.1 --web-port $WebPort --mcp-port $McpPort --mcp-stream-port $McpStreamPort --mode $McpMode"
|
|
67
|
+
if ($NoWeb) { $cmd = "$cmd --no-web" }
|
|
66
68
|
schtasks /Create /F /SC ONLOGON /RL LIMITED /TN $taskName /TR $cmd | Out-Null
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
Write-Output "zocket installed successfully."
|
|
70
72
|
Write-Output "zocket: $zocketBin"
|
|
71
73
|
Write-Output "ZOCKET_HOME=$ZocketHome"
|
|
72
|
-
|
|
74
|
+
if ($NoWeb) {
|
|
75
|
+
Write-Output "web panel: disabled"
|
|
76
|
+
} else {
|
|
77
|
+
Write-Output "web panel: http://127.0.0.1:$WebPort"
|
|
78
|
+
}
|
|
73
79
|
Write-Output "mcp sse: http://127.0.0.1:$McpPort/sse"
|
|
74
80
|
Write-Output "mcp http: http://127.0.0.1:$McpStreamPort/mcp"
|
|
@@ -15,6 +15,7 @@ MCP_STREAM_PORT="${MCP_STREAM_PORT:-18003}"
|
|
|
15
15
|
MCP_MODE="${MCP_MODE:-admin}" # metadata|admin
|
|
16
16
|
AUTOSTART="${AUTOSTART:-user}" # user|system|none
|
|
17
17
|
SERVICE_USER="${SERVICE_USER:-zocketd}"
|
|
18
|
+
WEB_ENABLED="${WEB_ENABLED:-true}"
|
|
18
19
|
|
|
19
20
|
usage() {
|
|
20
21
|
cat <<EOF
|
|
@@ -30,6 +31,7 @@ Options:
|
|
|
30
31
|
--mcp-port <port>
|
|
31
32
|
--mcp-stream-port <port>
|
|
32
33
|
--mcp-mode <metadata|admin>
|
|
34
|
+
--no-web
|
|
33
35
|
--autostart <user|system|none>
|
|
34
36
|
--service-user <name>
|
|
35
37
|
-h, --help
|
|
@@ -49,6 +51,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
49
51
|
--mcp-port) MCP_PORT="$2"; shift 2 ;;
|
|
50
52
|
--mcp-stream-port) MCP_STREAM_PORT="$2"; shift 2 ;;
|
|
51
53
|
--mcp-mode) MCP_MODE="$2"; shift 2 ;;
|
|
54
|
+
--no-web) WEB_ENABLED="false"; shift 1 ;;
|
|
52
55
|
--autostart) AUTOSTART="$2"; shift 2 ;;
|
|
53
56
|
--service-user) SERVICE_USER="$2"; shift 2 ;;
|
|
54
57
|
-h|--help) usage; exit 0 ;;
|
|
@@ -242,6 +245,9 @@ EOF
|
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
EXEC_START="${ZOCKET_BIN} start --host 127.0.0.1 --web-port ${WEB_PORT} --mcp-port ${MCP_PORT} --mcp-stream-port ${MCP_STREAM_PORT} --mode ${MCP_MODE}"
|
|
248
|
+
if [[ "$WEB_ENABLED" == "false" ]]; then
|
|
249
|
+
EXEC_START="${EXEC_START} --no-web"
|
|
250
|
+
fi
|
|
245
251
|
|
|
246
252
|
if [[ "$AUTOSTART" == "user" && "$OS" == linux* ]]; then
|
|
247
253
|
USER_UNIT_DIR="$HOME/.config/systemd/user"
|
|
@@ -270,7 +276,7 @@ zocket: ${ZOCKET_BIN}
|
|
|
270
276
|
ZOCKET_HOME=${ZOCKET_HOME_DIR}
|
|
271
277
|
|
|
272
278
|
Default ports:
|
|
273
|
-
web panel: http://127.0.0.1:${WEB_PORT}
|
|
279
|
+
web panel: http://127.0.0.1:${WEB_PORT}$( [[ "$WEB_ENABLED" == "false" ]] && printf ' (disabled)' )
|
|
274
280
|
MCP SSE: http://127.0.0.1:${MCP_PORT}/sse
|
|
275
281
|
MCP HTTP: http://127.0.0.1:${MCP_STREAM_PORT}/mcp
|
|
276
282
|
|