@ao_zorin/zocket 1.1.0 → 1.2.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 CHANGED
@@ -36,18 +36,23 @@ zocket init
36
36
 
37
37
  ```bash
38
38
  zocket init
39
- zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mode admin
39
+ zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
40
40
  ```
41
41
 
42
42
  Open `http://127.0.0.1:18001`.
43
43
 
44
- ## Codex (streamable HTTP)
44
+ ## CLI / TUI
45
45
 
46
- Codex requires streamable HTTP on `127.0.0.1:18003/mcp`. The Node CLI does **SSE only** today.
47
- Until streamable HTTP is implemented in Node, run the companion Python service (separate package):
46
+ Full CLI management:
47
+ ```bash
48
+ zocket projects list
49
+ zocket projects create myproj --description \"demo\"
50
+ zocket secrets set myproj API_KEY abc123 --description \"example\"
51
+ ```
48
52
 
53
+ Interactive TUI:
49
54
  ```bash
50
- python3 -m zocket mcp --transport streamable-http --mode admin --host 127.0.0.1 --port 18003
55
+ zocket tui
51
56
  ```
52
57
 
53
58
  ## Docs
package/dist/zocket.js CHANGED
@@ -3,11 +3,12 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
  import { createServer } from "http";
6
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
7
- import { dirname as dirname5 } from "path";
8
- import { randomBytes as randomBytes6 } from "crypto";
6
+ import { mkdirSync as mkdirSync5 } from "fs";
7
+ import { randomUUID } from "crypto";
9
8
  import { serve } from "@hono/node-server";
10
9
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
10
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
11
12
 
12
13
  // src/vault.ts
13
14
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
@@ -58,6 +59,11 @@ var VaultService = class {
58
59
  mkdirSync(dirname(this.vaultPath), { recursive: true });
59
60
  writeFileSync(this.vaultPath, encrypt(Buffer.from(JSON.stringify(data)), this.key));
60
61
  }
62
+ ensureExists() {
63
+ if (!existsSync(this.vaultPath)) {
64
+ this.save({ version: 1, projects: {} });
65
+ }
66
+ }
61
67
  async withLock(fn) {
62
68
  mkdirSync(dirname(this.lockFile), { recursive: true });
63
69
  if (!existsSync(this.lockFile)) writeFileSync(this.lockFile, "");
@@ -302,12 +308,12 @@ function runCommand(command, env, policy, maxChars = 500) {
302
308
  r.exit_code = proc.status ?? 1;
303
309
  return r;
304
310
  }
305
- function runScript(lang, code, env, maxChars = 500) {
306
- const ext = lang === "node" ? ".mjs" : ".py";
311
+ function runScript(_lang, code, env, maxChars = 500) {
312
+ const ext = ".mjs";
307
313
  const tmpFile = join(tmpdir(), `zkt-${randomBytes3(8).toString("hex")}${ext}`);
308
314
  try {
309
315
  writeFileSync3(tmpFile, code, "utf8");
310
- const bin = lang === "node" ? "node" : "python3";
316
+ const bin = "node";
311
317
  const proc = spawnSync(bin, [tmpFile], {
312
318
  env: { ...process.env, ...env },
313
319
  encoding: "utf8",
@@ -483,8 +489,8 @@ function envRefsIn(text) {
483
489
  const matches = [...text.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)].map((m) => m[1]);
484
490
  return [...new Set(matches)];
485
491
  }
486
- function buildCtx(text, lang) {
487
- return { text, envRefs: envRefsIn(text), hasNetwork: NETWORK_RE.test(text), lang };
492
+ function buildCtx(text) {
493
+ return { text, envRefs: envRefsIn(text), hasNetwork: NETWORK_RE.test(text) };
488
494
  }
489
495
  var RULES = [
490
496
  // ── Critical ────────────────────────────────────────────────────────────
@@ -577,13 +583,6 @@ var RULES = [
577
583
  weight: W.MEDIUM,
578
584
  test: (c) => /\b(printenv|env)\b[^|]*>/.test(c.text)
579
585
  },
580
- {
581
- id: "PYTHON_SUBPROCESS_EXFIL",
582
- description: "Python subprocess call invoking network tool (curl/wget/nc)",
583
- severity: "medium",
584
- weight: W.MEDIUM,
585
- test: (c) => c.lang === "python" && /subprocess\.(run|Popen|call|check_output)\([^)]*\b(curl|wget|nc\b|netcat)\b/.test(c.text)
586
- },
587
586
  // ── Low ──────────────────────────────────────────────────────────────────
588
587
  {
589
588
  id: "SINGLE_ENV_NETWORK",
@@ -648,9 +647,9 @@ var SecurityAnalyzer = class {
648
647
  const text = isBashC ? command[2] : command.join(" ");
649
648
  return this.analyze(buildCtx(text));
650
649
  }
651
- analyzeScript(lang, code) {
650
+ analyzeScript(_lang, code) {
652
651
  if (this.cfg.mode === "off") return allow();
653
- const ctx = buildCtx(code, lang);
652
+ const ctx = buildCtx(code);
654
653
  return this.analyze(ctx);
655
654
  }
656
655
  analyze(ctx) {
@@ -870,18 +869,18 @@ function buildCatalog(services) {
870
869
  },
871
870
  {
872
871
  name: "run_script",
873
- summary: "Run an inline node/python script with project secrets injected as env vars. Prefer over multiple run_with_project_env calls.",
872
+ summary: "Run an inline node script with project secrets injected as env vars. Prefer over multiple run_with_project_env calls.",
874
873
  register: (server) => {
875
874
  server.tool(
876
875
  "run_script",
877
876
  [
878
- "Run an inline script with project secrets available as environment variables.",
877
+ "Run an inline node script with project secrets available as environment variables.",
879
878
  "Use this instead of multiple run_with_project_env calls \u2014 write the full logic in one script.",
880
879
  "Filesystem is NOT shared between calls. Secret values never appear in this conversation."
881
880
  ].join(" "),
882
881
  {
883
882
  project: z.string().describe("Project name"),
884
- lang: z.enum(["node", "python"]).describe("Script language"),
883
+ lang: z.enum(["node"]).describe("Script language"),
885
884
  code: z.string().min(1).describe("Full script source code"),
886
885
  max_chars: z.number().int().min(1).max(32e3).optional().describe("Max output chars (default ~500)"),
887
886
  confirm: z.boolean().optional().describe("Set to true to confirm execution of a medium-risk script after reviewing the warning")
@@ -1922,7 +1921,10 @@ function lockPath(home = zocketHome()) {
1922
1921
  return join3(home, "vault.lock");
1923
1922
  }
1924
1923
 
1925
- // src/cli.ts
1924
+ // src/keys.ts
1925
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
1926
+ import { dirname as dirname5 } from "path";
1927
+ import { randomBytes as randomBytes6 } from "crypto";
1926
1928
  function loadOrCreateKey(keyFile) {
1927
1929
  if (existsSync5(keyFile)) {
1928
1930
  const raw = readFileSync5(keyFile, "utf8").trim();
@@ -1933,13 +1935,175 @@ function loadOrCreateKey(keyFile) {
1933
1935
  writeFileSync5(keyFile, key.toString("hex"), { mode: 384 });
1934
1936
  return key;
1935
1937
  }
1936
- async function cmdStart(opts) {
1938
+
1939
+ // src/tui.ts
1940
+ import readline from "readline/promises";
1941
+ import { stdin as input, stdout as output } from "process";
1942
+ async function promptMenu(rl, title, options) {
1943
+ output.write(`
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";
1959
+ }
1960
+ async function pickProject(rl, vault) {
1961
+ const rows = await vault.listProjects();
1962
+ if (!rows.length) {
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;
1969
+ }
1970
+ async function showProjects(vault) {
1971
+ const rows = await vault.listProjects();
1972
+ if (!rows.length) {
1973
+ output.write("No projects\n");
1974
+ return;
1975
+ }
1976
+ rows.forEach((r) => {
1977
+ output.write(`${r.name} ${r.secret_count} ${r.folder_path ?? ""}
1978
+ `);
1979
+ });
1980
+ }
1981
+ async function showSecrets(vault, project, showValues) {
1982
+ const rows = await vault.listSecrets(project);
1983
+ if (!rows.length) {
1984
+ output.write("No secrets\n");
1985
+ return;
1986
+ }
1987
+ for (const r of rows) {
1988
+ if (showValues) {
1989
+ const v = await vault.getSecretValue(project, r.key);
1990
+ output.write(`${r.key} ${v} ${r.description ?? ""}
1991
+ `);
1992
+ } else {
1993
+ output.write(`${r.key} ${r.description ?? ""}
1994
+ `);
1995
+ }
1996
+ }
1997
+ }
1998
+ async function runTui(services) {
1999
+ const rl = readline.createInterface({ input, output });
2000
+ const { vault } = services;
2001
+ try {
2002
+ while (true) {
2003
+ const main = await promptMenu(rl, "Zocket TUI", [
2004
+ "Projects",
2005
+ "Secrets",
2006
+ "Exit"
2007
+ ]);
2008
+ if (main === 2) break;
2009
+ if (main === 0) {
2010
+ const idx = await promptMenu(rl, "Projects", [
2011
+ "List",
2012
+ "Create",
2013
+ "Delete",
2014
+ "Set folder path",
2015
+ "Set allowed domains",
2016
+ "Back"
2017
+ ]);
2018
+ if (idx === 5) continue;
2019
+ if (idx === 0) await showProjects(vault);
2020
+ if (idx === 1) {
2021
+ const name = await promptInput(rl, "Project name");
2022
+ const desc = await promptInput(rl, "Description", "");
2023
+ const folder = await promptInput(rl, "Folder path", "");
2024
+ await vault.createProject(name, desc);
2025
+ if (folder) await vault.setFolder(name, folder);
2026
+ output.write("Project created\n");
2027
+ }
2028
+ if (idx === 2) {
2029
+ const name = await pickProject(rl, vault);
2030
+ if (!name) continue;
2031
+ if (await promptConfirm(rl, `Delete ${name}?`)) {
2032
+ await vault.deleteProject(name);
2033
+ output.write("Project deleted\n");
2034
+ }
2035
+ }
2036
+ if (idx === 3) {
2037
+ const name = await pickProject(rl, vault);
2038
+ if (!name) continue;
2039
+ const path = await promptInput(rl, "Folder path (empty to clear)", "");
2040
+ await vault.setFolder(name, path || void 0);
2041
+ output.write("Folder updated\n");
2042
+ }
2043
+ if (idx === 4) {
2044
+ const name = await pickProject(rl, vault);
2045
+ if (!name) continue;
2046
+ const domains = await promptInput(rl, "Allowed domains (comma-separated, empty to clear)", "");
2047
+ const value = domains ? domains.split(",").map((s) => s.trim()).filter(Boolean) : null;
2048
+ await vault.setAllowedDomains(name, value);
2049
+ output.write("Allowed domains updated\n");
2050
+ }
2051
+ }
2052
+ if (main === 1) {
2053
+ const project = await pickProject(rl, vault);
2054
+ if (!project) continue;
2055
+ const idx = await promptMenu(rl, `Secrets for ${project}`, [
2056
+ "List (no values)",
2057
+ "List (with values)",
2058
+ "Add or update",
2059
+ "Get value",
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
+ }
2086
+ }
2087
+ }
2088
+ } finally {
2089
+ rl.close();
2090
+ }
2091
+ }
2092
+
2093
+ // src/cli.ts
2094
+ function createServices() {
1937
2095
  const home = zocketHome();
1938
- mkdirSync4(home, { recursive: true });
2096
+ mkdirSync5(home, { recursive: true });
1939
2097
  const key = loadOrCreateKey(keyPath());
1940
2098
  const vault = new VaultService(vaultPath(), lockPath(), key);
2099
+ vault.ensureExists();
1941
2100
  const config = new ConfigStore(configPath());
1942
2101
  const audit = new AuditLogger(auditPath());
2102
+ config.ensureExists();
2103
+ return { vault, config, audit };
2104
+ }
2105
+ async function cmdStart(opts) {
2106
+ const { vault, config, audit } = createServices();
1943
2107
  const cfg = config.ensureExists();
1944
2108
  const mode = opts.mode === "admin" ? "admin" : "metadata";
1945
2109
  const services = { vault, config, audit, mode };
@@ -1976,19 +2140,169 @@ async function cmdStart(opts) {
1976
2140
  });
1977
2141
  mcpHttp.listen(opts.mcpPort, opts.host, () => {
1978
2142
  console.log(`[zocket] mcp http://${opts.host}:${opts.mcpPort}/sse (mode: ${mode}, loading: ${cfg.mcp_loading})`);
2143
+ });
2144
+ const streamableSessions = /* @__PURE__ */ new Map();
2145
+ const streamableHttp = createServer(async (req, res) => {
2146
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
2147
+ if (url.pathname !== "/mcp") {
2148
+ res.writeHead(404).end("Not found");
2149
+ return;
2150
+ }
2151
+ const method = (req.method ?? "GET").toUpperCase();
2152
+ let parsedBody = void 0;
2153
+ if (method === "POST") {
2154
+ let raw = "";
2155
+ for await (const chunk of req) {
2156
+ raw += chunk;
2157
+ }
2158
+ if (raw.trim().length > 0) {
2159
+ try {
2160
+ parsedBody = JSON.parse(raw);
2161
+ } catch {
2162
+ res.writeHead(400, { "Content-Type": "application/json" });
2163
+ res.end(JSON.stringify({
2164
+ jsonrpc: "2.0",
2165
+ error: { code: -32700, message: "Parse error" },
2166
+ id: null
2167
+ }));
2168
+ return;
2169
+ }
2170
+ }
2171
+ }
2172
+ const header = req.headers["mcp-session-id"];
2173
+ const sessionId = Array.isArray(header) ? header[0] : header;
2174
+ let transport;
2175
+ if (sessionId && streamableSessions.has(sessionId)) {
2176
+ transport = streamableSessions.get(sessionId);
2177
+ } else if (!sessionId && method === "POST" && parsedBody && isInitializeRequest(parsedBody)) {
2178
+ transport = new StreamableHTTPServerTransport({
2179
+ sessionIdGenerator: () => randomUUID(),
2180
+ onsessioninitialized: (sid) => {
2181
+ if (transport) streamableSessions.set(sid, transport);
2182
+ }
2183
+ });
2184
+ transport.onclose = () => {
2185
+ const sid = transport?.sessionId;
2186
+ if (sid && streamableSessions.has(sid)) streamableSessions.delete(sid);
2187
+ };
2188
+ const mcpServer = createMcpServer(services, { loading: cfg.mcp_loading });
2189
+ mcpServer.connect(transport).catch((e) => {
2190
+ console.error("[zocket] MCP streamable connect error:", e);
2191
+ });
2192
+ } else {
2193
+ res.writeHead(400, { "Content-Type": "application/json" });
2194
+ res.end(JSON.stringify({
2195
+ jsonrpc: "2.0",
2196
+ error: { code: -32e3, message: "Bad Request: No valid session ID provided" },
2197
+ id: null
2198
+ }));
2199
+ return;
2200
+ }
2201
+ try {
2202
+ await transport.handleRequest(req, res, parsedBody);
2203
+ } catch (e) {
2204
+ console.error("[zocket] MCP streamable request error:", e);
2205
+ if (!res.headersSent) {
2206
+ res.writeHead(500, { "Content-Type": "application/json" });
2207
+ res.end(JSON.stringify({
2208
+ jsonrpc: "2.0",
2209
+ error: { code: -32603, message: "Internal server error" },
2210
+ id: null
2211
+ }));
2212
+ }
2213
+ }
2214
+ });
2215
+ streamableHttp.listen(opts.mcpStreamPort, opts.host, () => {
2216
+ console.log(`[zocket] mcp http://${opts.host}:${opts.mcpStreamPort}/mcp (streamable-http)`);
1979
2217
  console.log(`[zocket] ready \u2014 vault: ${vaultPath()}`);
1980
2218
  });
1981
2219
  }
1982
2220
  function buildCli() {
1983
2221
  const program = new Command("zocket").description("Local encrypted vault + MCP server for AI agent workflows").version("1.0.0");
1984
- program.command("start").description("Start web panel and MCP SSE server").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("--mode <mode>", "MCP mode (metadata|admin)", "admin").action(async (opts) => {
2222
+ program.command("init").description("Initialize vault and config").action(async () => {
2223
+ createServices();
2224
+ console.log(`[zocket] initialized \u2014 vault: ${vaultPath()}`);
2225
+ });
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) => {
1985
2227
  await cmdStart({
1986
2228
  host: opts.host,
1987
2229
  webPort: parseInt(opts.webPort, 10),
1988
2230
  mcpPort: parseInt(opts.mcpPort, 10),
2231
+ mcpStreamPort: parseInt(opts.mcpStreamPort, 10),
1989
2232
  mode: opts.mode
1990
2233
  });
1991
2234
  });
2235
+ const projects = program.command("projects").description("Manage projects");
2236
+ projects.command("list").action(async () => {
2237
+ const { vault } = createServices();
2238
+ const rows = await vault.listProjects();
2239
+ if (!rows.length) {
2240
+ console.log("No projects");
2241
+ return;
2242
+ }
2243
+ for (const r of rows) {
2244
+ console.log(`${r.name} ${r.secret_count} ${r.folder_path ?? ""}`);
2245
+ }
2246
+ });
2247
+ projects.command("create <name>").option("--description <text>", "Description", "").option("--folder <path>", "Folder path", "").action(async (name, opts) => {
2248
+ const { vault } = createServices();
2249
+ await vault.createProject(name, opts.description ?? "");
2250
+ if (opts.folder) await vault.setFolder(name, opts.folder);
2251
+ console.log("Project created:", name);
2252
+ });
2253
+ projects.command("delete <name>").action(async (name) => {
2254
+ const { vault } = createServices();
2255
+ await vault.deleteProject(name);
2256
+ console.log("Project deleted:", name);
2257
+ });
2258
+ projects.command("set-folder <name> [path]").description('Set or clear folder path (use "-" to clear)').action(async (name, path) => {
2259
+ const { vault } = createServices();
2260
+ const value = path && path !== "-" ? path : void 0;
2261
+ await vault.setFolder(name, value);
2262
+ console.log("Folder updated:", name);
2263
+ });
2264
+ projects.command("set-domains <name> [domains]").description('Set or clear allowed domains (comma-separated, use "-" to clear)').action(async (name, domains) => {
2265
+ const { vault } = createServices();
2266
+ const value = domains && domains !== "-" ? domains.split(",").map((s) => s.trim()).filter(Boolean) : null;
2267
+ await vault.setAllowedDomains(name, value);
2268
+ console.log("Domains updated:", name);
2269
+ });
2270
+ const secrets = program.command("secrets").description("Manage secrets");
2271
+ secrets.command("list <project>").option("--show-values", "Include secret values", false).action(async (project, opts) => {
2272
+ const { vault } = createServices();
2273
+ const rows = await vault.listSecrets(project);
2274
+ if (!rows.length) {
2275
+ console.log("No secrets");
2276
+ return;
2277
+ }
2278
+ for (const r of rows) {
2279
+ if (opts.showValues) {
2280
+ const v = await vault.getSecretValue(project, r.key);
2281
+ console.log(`${r.key} ${v} ${r.description ?? ""}`);
2282
+ } else {
2283
+ console.log(`${r.key} ${r.description ?? ""}`);
2284
+ }
2285
+ }
2286
+ });
2287
+ secrets.command("get <project> <key>").action(async (project, key) => {
2288
+ const { vault } = createServices();
2289
+ const v = await vault.getSecretValue(project, key);
2290
+ console.log(v);
2291
+ });
2292
+ secrets.command("set <project> <key> <value>").option("--description <text>", "Description", "").action(async (project, key, value, opts) => {
2293
+ const { vault } = createServices();
2294
+ await vault.setSecret(project, key, value, opts.description ?? "");
2295
+ console.log("Secret saved:", key);
2296
+ });
2297
+ secrets.command("delete <project> <key>").action(async (project, key) => {
2298
+ const { vault } = createServices();
2299
+ await vault.deleteSecret(project, key);
2300
+ console.log("Secret deleted:", key);
2301
+ });
2302
+ program.command("tui").description("Interactive terminal UI for full management").action(async () => {
2303
+ const { vault, config, audit } = createServices();
2304
+ await runTui({ vault, config, audit });
2305
+ });
1992
2306
  return program;
1993
2307
  }
1994
2308
 
@@ -6,22 +6,22 @@ This is the file you can send to an AI coding agent so it can deploy zocket end-
6
6
 
7
7
  If this repo is already present locally:
8
8
  ```bash
9
- python3 scripts/ai-autodeploy.py --repo-url https://github.com/your-org/zocket.git --repo-ref main
9
+ bash scripts/install-zocket.sh --source local
10
10
  ```
11
11
 
12
- If only the file is available, the script will clone the repo and run the platform installer.
12
+ If only the file is available, the script will fetch and run the platform installer:
13
+ ```bash
14
+ curl -fsSL https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.sh | bash
15
+ ```
13
16
 
14
17
  ## Agent task prompt (copy/paste)
15
18
 
16
19
  ```text
17
20
  Deploy zocket on this machine with secure defaults:
18
21
  1) Detect OS and shell.
19
- 2) Run scripts/ai-autodeploy.py with:
20
- --lang en
21
- --web-port 18001
22
- --mcp-port 18002
23
- --mcp-mode metadata
24
- --autostart user (Linux/macOS) or enabled startup task (Windows)
22
+ 2) Run installer:
23
+ - Linux/macOS: curl -fsSL https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.sh | bash
24
+ - Windows: irm https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.ps1 | iex
25
25
  3) Verify:
26
26
  - web panel on http://127.0.0.1:18001
27
27
  - MCP SSE endpoint on http://127.0.0.1:18002/sse (Claude Code)
@@ -33,11 +33,7 @@ Deploy zocket on this machine with secure defaults:
33
33
  ## Optional production profile (Linux system services)
34
34
 
35
35
  ```bash
36
- python3 scripts/ai-autodeploy.py \
37
- --repo-url https://github.com/your-org/zocket.git \
38
- --repo-ref main \
39
- --autostart system \
40
- --zocket-home /var/lib/zocket
36
+ bash scripts/install-zocket.sh --source git --repo-url https://github.com/aozorin/zocket.git --repo-ref main --autostart system --zocket-home /var/lib/zocket
41
37
  ```
42
38
 
43
39
  ## Post-deploy checklist for agent
@@ -1,4 +1,4 @@
1
- # Git + npm + Python Release Guide
1
+ # Git + npm Release Guide
2
2
 
3
3
  ## 1) Initialize git project
4
4
 
@@ -17,15 +17,7 @@ git push -u origin main
17
17
  bash scripts/release-check.sh
18
18
  ```
19
19
 
20
- ## 3) Publish Python package (PyPI)
21
-
22
- ```bash
23
- python3 -m pip install --upgrade build twine
24
- python3 -m build
25
- twine upload dist/*
26
- ```
27
-
28
- ## 4) Publish npm package
20
+ ## 3) Publish npm package
29
21
 
30
22
  Update metadata in `package.json`:
31
23
  - `name`
@@ -39,15 +31,14 @@ npm login
39
31
  npm publish --access public
40
32
  ```
41
33
 
42
- ## 5) Tag release
34
+ ## 4) Tag release
43
35
 
44
36
  ```bash
45
37
  git tag -a v1.0.0 -m "zocket v1.0.0"
46
38
  git push origin v1.0.0
47
39
  ```
48
40
 
49
- ## 6) Optional GitHub Release artifacts
41
+ ## 5) Optional GitHub Release artifacts
50
42
 
51
43
  Upload:
52
- - Python wheels/sdist from `dist/`
53
44
  - npm package tarball from `npm pack`
package/docs/INSTALL.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # Install Guide (Windows / Linux / macOS)
2
2
 
3
- This guide installs **zocket** as:
3
+ This guide installs **zocket** (Node.js) as:
4
4
  - local web panel on `127.0.0.1:18001`
5
5
  - MCP SSE server on `127.0.0.1:18002/sse` (Claude Code)
6
6
  - MCP streamable HTTP server on `127.0.0.1:18003/mcp` (Codex)
7
- - optional MCP stdio server for local CLI use
8
7
 
9
8
  ## 1) Quick Install (recommended)
10
9
 
11
10
  ### Linux and macOS
12
11
  ```bash
13
- curl -fsSL https://raw.githubusercontent.com/your-org/zocket/main/scripts/install-zocket.sh | bash
12
+ curl -fsSL https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.sh | bash
14
13
  ```
15
14
 
16
15
  If you run from a local clone:
@@ -20,7 +19,7 @@ bash scripts/install-zocket.sh --source local
20
19
 
21
20
  ### Windows (PowerShell)
22
21
  ```powershell
23
- irm https://raw.githubusercontent.com/your-org/zocket/main/scripts/install-zocket.ps1 | iex
22
+ irm https://raw.githubusercontent.com/aozorin/zocket/main/scripts/install-zocket.ps1 | iex
24
23
  ```
25
24
 
26
25
  If you run from a local clone:
@@ -32,16 +31,15 @@ powershell -ExecutionPolicy Bypass -File .\scripts\install-zocket.ps1 -Source Lo
32
31
 
33
32
  ### Debian/Ubuntu and Debian-based
34
33
  Installer auto-installs:
35
- - `python3`
36
- - `python3-venv`
37
- - `python3-pip`
34
+ - `nodejs`
35
+ - `npm`
38
36
  - `git`
39
37
  - `curl`
40
38
 
41
39
  Equivalent manual install:
42
40
  ```bash
43
41
  sudo apt-get update
44
- sudo apt-get install -y python3 python3-venv python3-pip git curl
42
+ sudo apt-get install -y nodejs npm git curl
45
43
  ```
46
44
 
47
45
  ### Other Linux distros
@@ -52,22 +50,21 @@ Installer supports:
52
50
  - `apk` (Alpine)
53
51
 
54
52
  If your distro is unsupported, install manually:
55
- - Python `>=3.10`
56
- - `pip`
57
- - `venv`
53
+ - Node.js `>=18`
54
+ - `npm`
58
55
  - `git`
59
56
 
60
57
  ## 3) macOS details
61
58
 
62
59
  Installer uses Homebrew when dependencies are missing:
63
60
  ```bash
64
- brew install python git curl
61
+ brew install node git curl
65
62
  ```
66
63
 
67
64
  ## 4) Windows details
68
65
 
69
66
  Requirements:
70
- - Python 3.10+ (recommended from `python.org` or `winget`)
67
+ - Node.js 18+ (recommended from `nodejs.org` or `winget`)
71
68
  - Git for Windows
72
69
 
73
70
  Autostart (enabled by default):
@@ -75,10 +72,8 @@ Autostart (enabled by default):
75
72
  powershell -ExecutionPolicy Bypass -File .\scripts\install-zocket.ps1
76
73
  ```
77
74
 
78
- This creates scheduled tasks:
79
- - `ZocketWeb`
80
- - `ZocketMcpSse`
81
- - `ZocketMcpStreamable`
75
+ This creates scheduled task:
76
+ - `Zocket`
82
77
 
83
78
  Disable autostart:
84
79
  ```powershell
@@ -87,128 +82,53 @@ powershell -ExecutionPolicy Bypass -File .\scripts\install-zocket.ps1 -EnableAut
87
82
 
88
83
  ## 5) NPM package usage
89
84
 
90
- This repo now includes an npm wrapper package.
91
-
92
85
  Global install from npm:
93
86
  ```bash
94
- npm i -g @zocket/cli
95
- zocket setup
96
- ```
97
-
98
- Or install from your git repo (example):
99
- ```bash
100
- npm i -g github:your-org/zocket
101
- ```
102
-
103
- First-run setup:
104
- ```bash
105
- zocket setup
87
+ npm i -g @ao_zorin/zocket
106
88
  ```
107
89
 
108
90
  Then use normal CLI:
109
91
  ```bash
110
92
  zocket init
111
- zocket web --host 127.0.0.1 --port 18001
112
- zocket mcp --transport sse --mode metadata --host 127.0.0.1 --port 18002
113
- zocket mcp --transport streamable-http --mode metadata --host 127.0.0.1 --port 18003
93
+ zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
114
94
  ```
115
95
 
116
96
  ## 6) Systemd hardening on Linux (production)
117
97
 
118
98
  If you install with `--autostart system`, the installer creates and enables:
119
- - `zocket-web.service` (web panel on 18001)
120
- - `zocket-mcp-sse.service` (Claude Code, 18002)
121
- - `zocket-mcp-http.service` (Codex, 18003)
99
+ - `zocket.service` (web + SSE + streamable HTTP)
122
100
 
123
101
  Check:
124
102
  ```bash
125
- systemctl status zocket-web.service --no-pager
126
- systemctl status zocket-mcp-sse.service --no-pager
127
- systemctl status zocket-mcp-http.service --no-pager
103
+ systemctl status zocket.service --no-pager
128
104
  ```
129
105
 
130
106
  ### Linux user-level autostart (no root)
131
107
  ```bash
132
- systemctl --user enable --now zocket-web.service
133
- systemctl --user enable --now zocket-mcp-sse.service
134
- systemctl --user enable --now zocket-mcp-http.service
135
- systemctl --user status zocket-web.service --no-pager
136
- systemctl --user status zocket-mcp-sse.service --no-pager
137
- systemctl --user status zocket-mcp-http.service --no-pager
108
+ systemctl --user enable --now zocket.service
109
+ systemctl --user status zocket.service --no-pager
138
110
  ```
139
111
 
140
112
  ### macOS launchd autostart (installed by script)
141
113
  Installer creates and loads:
142
- - `~/Library/LaunchAgents/dev.zocket.web.plist`
143
- - `~/Library/LaunchAgents/dev.zocket.mcp-sse.plist`
144
- - `~/Library/LaunchAgents/dev.zocket.mcp-streamable.plist`
145
-
146
- If you need to install manually, use:
147
-
148
- ```xml
149
- <?xml version="1.0" encoding="UTF-8"?>
150
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
151
- <plist version="1.0">
152
- <dict>
153
- <key>Label</key><string>dev.zocket.web</string>
154
- <key>ProgramArguments</key>
155
- <array>
156
- <string>/Users/YOUR_USER/.local/share/zocket/venv/bin/python3</string>
157
- <string>-m</string><string>zocket</string>
158
- <string>web</string><string>--host</string><string>127.0.0.1</string>
159
- <string>--port</string><string>18001</string>
160
- </array>
161
- <key>EnvironmentVariables</key>
162
- <dict>
163
- <key>ZOCKET_HOME</key><string>/Users/YOUR_USER/.zocket</string>
164
- </dict>
165
- <key>RunAtLoad</key><true/>
166
- <key>KeepAlive</key><true/>
167
- </dict>
168
- </plist>
169
- ```
170
-
171
- SSE MCP (`dev.zocket.mcp-sse.plist`):
172
- ```xml
173
- <?xml version="1.0" encoding="UTF-8"?>
174
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
175
- <plist version="1.0">
176
- <dict>
177
- <key>Label</key><string>dev.zocket.mcp-sse</string>
178
- <key>ProgramArguments</key>
179
- <array>
180
- <string>/Users/YOUR_USER/.local/share/zocket/venv/bin/python3</string>
181
- <string>-m</string><string>zocket</string>
182
- <string>mcp</string><string>--transport</string><string>sse</string>
183
- <string>--mode</string><string>metadata</string>
184
- <string>--host</string><string>127.0.0.1</string>
185
- <string>--port</string><string>18002</string>
186
- </array>
187
- <key>EnvironmentVariables</key>
188
- <dict>
189
- <key>ZOCKET_HOME</key><string>/Users/YOUR_USER/.zocket</string>
190
- </dict>
191
- <key>RunAtLoad</key><true/>
192
- <key>KeepAlive</key><true/>
193
- </dict>
194
- </plist>
195
- ```
114
+ - `~/Library/LaunchAgents/dev.zocket.plist`
196
115
 
197
- Streamable HTTP MCP (`dev.zocket.mcp-streamable.plist`):
116
+ Manual example:
198
117
  ```xml
199
118
  <?xml version="1.0" encoding="UTF-8"?>
200
119
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
201
120
  <plist version="1.0">
202
121
  <dict>
203
- <key>Label</key><string>dev.zocket.mcp-streamable</string>
122
+ <key>Label</key><string>dev.zocket</string>
204
123
  <key>ProgramArguments</key>
205
124
  <array>
206
- <string>/Users/YOUR_USER/.local/share/zocket/venv/bin/python3</string>
207
- <string>-m</string><string>zocket</string>
208
- <string>mcp</string><string>--transport</string><string>streamable-http</string>
209
- <string>--mode</string><string>metadata</string>
125
+ <string>/usr/local/bin/zocket</string>
126
+ <string>start</string>
210
127
  <string>--host</string><string>127.0.0.1</string>
211
- <string>--port</string><string>18003</string>
128
+ <string>--web-port</string><string>18001</string>
129
+ <string>--mcp-port</string><string>18002</string>
130
+ <string>--mcp-stream-port</string><string>18003</string>
131
+ <string>--mode</string><string>admin</string>
212
132
  </array>
213
133
  <key>EnvironmentVariables</key>
214
134
  <dict>
@@ -220,11 +140,9 @@ Streamable HTTP MCP (`dev.zocket.mcp-streamable.plist`):
220
140
  </plist>
221
141
  ```
222
142
 
223
- Load services:
143
+ Load service:
224
144
  ```bash
225
- launchctl load ~/Library/LaunchAgents/dev.zocket.web.plist
226
- launchctl load ~/Library/LaunchAgents/dev.zocket.mcp-sse.plist
227
- launchctl load ~/Library/LaunchAgents/dev.zocket.mcp-streamable.plist
145
+ launchctl load ~/Library/LaunchAgents/dev.zocket.plist
228
146
  ```
229
147
 
230
148
  ### Windows autostart (Task Scheduler)
@@ -234,13 +152,9 @@ powershell -ExecutionPolicy Bypass -File .\scripts\install-zocket.ps1 -EnableAut
234
152
  ```
235
153
 
236
154
  Or create manually:
237
- - task `ZocketWeb` on logon
238
- - task `ZocketMcpSse` on logon
239
- - task `ZocketMcpStreamable` on logon
240
- - actions:
241
- - `python -m zocket web --host 127.0.0.1 --port 18001`
242
- - `python -m zocket mcp --transport sse --mode metadata --host 127.0.0.1 --port 18002`
243
- - `python -m zocket mcp --transport streamable-http --mode metadata --host 127.0.0.1 --port 18003`
155
+ - task `Zocket` on logon
156
+ - action:
157
+ - `zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin`
244
158
 
245
159
  ## 7) First web open
246
160
 
@@ -255,5 +169,5 @@ Open `http://127.0.0.1:18001` and choose one:
255
169
  curl -I http://127.0.0.1:18001/login
256
170
  curl -I http://127.0.0.1:18002/sse
257
171
  curl -I http://127.0.0.1:18003/mcp
258
- zocket mcp --transport stdio --mode metadata
172
+ zocket start --host 127.0.0.1 --web-port 18001 --mcp-port 18002 --mcp-stream-port 18003 --mode admin
259
173
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ao_zorin/zocket",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Local encrypted vault + web panel + MCP server for AI agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,8 @@
1
1
  Param(
2
- [ValidateSet("Auto", "Local", "Git", "PyPI")]
2
+ [ValidateSet("Auto", "Local", "Git", "Npm")]
3
3
  [string]$Source = "Auto",
4
- [string]$RepoUrl = "https://github.com/your-org/zocket.git",
4
+ [string]$RepoUrl = "https://github.com/aozorin/zocket.git",
5
5
  [string]$RepoRef = "main",
6
- [string]$InstallRoot = "$env:LOCALAPPDATA\zocket",
7
6
  [string]$ZocketHome = "$env:USERPROFILE\.zocket",
8
7
  [ValidateSet("en", "ru")]
9
8
  [string]$Lang = "en",
@@ -11,105 +10,64 @@ Param(
11
10
  [int]$McpPort = 18002,
12
11
  [int]$McpStreamPort = 18003,
13
12
  [ValidateSet("metadata", "admin")]
14
- [string]$McpMode = "metadata",
13
+ [string]$McpMode = "admin",
15
14
  [bool]$EnableAutostart = $true
16
15
  )
17
16
 
18
17
  $ErrorActionPreference = "Stop"
19
18
 
20
- function Resolve-Python {
21
- if (Get-Command py -ErrorAction SilentlyContinue) {
22
- return @{Cmd = "py"; Prefix = @("-3")}
23
- }
24
- if (Get-Command python -ErrorAction SilentlyContinue) {
25
- return @{Cmd = "python"; Prefix = @()}
19
+ function Ensure-Dir([string]$Path) {
20
+ if (-not (Test-Path -LiteralPath $Path)) {
21
+ New-Item -ItemType Directory -Path $Path | Out-Null
26
22
  }
27
- throw "Python 3.10+ not found. Install Python and rerun."
28
23
  }
29
24
 
30
- function Run-Step([string]$Cmd, [string[]]$Args) {
31
- & $Cmd @Args
32
- if ($LASTEXITCODE -ne 0) {
33
- throw "Command failed: $Cmd $($Args -join ' ')"
25
+ function Ensure-Node {
26
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
27
+ throw "Node.js not found. Install Node.js 18+ and rerun."
34
28
  }
35
- }
36
-
37
- function Ensure-Dir([string]$Path) {
38
- if (-not (Test-Path -LiteralPath $Path)) {
39
- New-Item -ItemType Directory -Path $Path | Out-Null
29
+ if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
30
+ throw "npm not found. Install Node.js 18+ (includes npm) and rerun."
40
31
  }
41
32
  }
42
33
 
34
+ Ensure-Node
35
+
43
36
  $repoRoot = Split-Path -Parent $PSScriptRoot
44
37
  if ($Source -eq "Auto") {
45
- if (Test-Path -LiteralPath (Join-Path $repoRoot "pyproject.toml")) {
38
+ if (Test-Path -LiteralPath (Join-Path $repoRoot "package.json")) {
46
39
  $Source = "Local"
47
40
  } else {
48
- $Source = "Git"
41
+ $Source = "Npm"
49
42
  }
50
43
  }
51
44
 
52
- Ensure-Dir $InstallRoot
53
- $srcDir = Join-Path $InstallRoot "src"
54
-
55
- $pkgSource = $null
56
45
  if ($Source -eq "Local") {
57
- $pkgSource = $repoRoot
46
+ npm i -g $repoRoot
58
47
  } elseif ($Source -eq "Git") {
59
- if (Test-Path -LiteralPath (Join-Path $srcDir ".git")) {
60
- Run-Step "git" @("-C", $srcDir, "fetch", "--all", "--tags")
61
- Run-Step "git" @("-C", $srcDir, "checkout", $RepoRef)
62
- Run-Step "git" @("-C", $srcDir, "pull", "--ff-only")
63
- } else {
64
- if (Test-Path -LiteralPath $srcDir) {
65
- Remove-Item -LiteralPath $srcDir -Recurse -Force
66
- }
67
- Run-Step "git" @("clone", "--depth", "1", "--branch", $RepoRef, $RepoUrl, $srcDir)
68
- }
69
- $pkgSource = $srcDir
48
+ npm i -g "git+$RepoUrl#$RepoRef"
70
49
  } else {
71
- $pkgSource = "zocket"
50
+ npm i -g @ao_zorin/zocket
72
51
  }
73
52
 
74
- $py = Resolve-Python
75
- $venvDir = Join-Path $InstallRoot "venv"
76
- $venvPy = Join-Path $venvDir "Scripts\python.exe"
77
- $zocketExe = Join-Path $venvDir "Scripts\zocket.exe"
78
-
79
- Run-Step $py.Cmd ($py.Prefix + @("-m", "venv", $venvDir))
80
- Run-Step $venvPy @("-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel")
81
-
82
- if ($Source -eq "PyPI") {
83
- Run-Step $venvPy @("-m", "pip", "install", "--upgrade", $pkgSource)
84
- } else {
85
- Run-Step $venvPy @("-m", "pip", "install", "--upgrade", $pkgSource)
53
+ $zocketBin = (Get-Command zocket).Source
54
+ if (-not $zocketBin) {
55
+ throw "zocket binary not found after install"
86
56
  }
87
57
 
88
58
  Ensure-Dir $ZocketHome
89
59
  $env:ZOCKET_HOME = $ZocketHome
90
60
 
91
- if (-not (Test-Path -LiteralPath (Join-Path $ZocketHome "vault.enc"))) {
92
- Run-Step $zocketExe @("init")
93
- }
94
- Run-Step $zocketExe @("config", "set-language", $Lang)
61
+ & $zocketBin init | Out-Null
95
62
 
96
63
  if ($EnableAutostart) {
97
- $webTask = "ZocketWeb"
98
- $mcpSseTask = "ZocketMcpSse"
99
- $mcpStreamTask = "ZocketMcpStreamable"
100
-
101
- $webCmd = "`"$venvPy`" -m zocket web --host 127.0.0.1 --port $WebPort"
102
- $mcpSseCmd = "`"$venvPy`" -m zocket mcp --transport sse --mode $McpMode --host 127.0.0.1 --port $McpPort"
103
- $mcpStreamCmd = "`"$venvPy`" -m zocket mcp --transport streamable-http --mode $McpMode --host 127.0.0.1 --port $McpStreamPort"
104
-
105
- schtasks /Create /F /SC ONLOGON /RL LIMITED /TN $webTask /TR $webCmd | Out-Null
106
- schtasks /Create /F /SC ONLOGON /RL LIMITED /TN $mcpSseTask /TR $mcpSseCmd | Out-Null
107
- schtasks /Create /F /SC ONLOGON /RL LIMITED /TN $mcpStreamTask /TR $mcpStreamCmd | Out-Null
64
+ $taskName = "Zocket"
65
+ $cmd = "\"$zocketBin\" start --host 127.0.0.1 --web-port $WebPort --mcp-port $McpPort --mcp-stream-port $McpStreamPort --mode $McpMode"
66
+ schtasks /Create /F /SC ONLOGON /RL LIMITED /TN $taskName /TR $cmd | Out-Null
108
67
  }
109
68
 
110
69
  Write-Output "zocket installed successfully."
111
- Write-Output "venv: $venvDir"
112
- Write-Output "zocket: $zocketExe"
70
+ Write-Output "zocket: $zocketBin"
113
71
  Write-Output "ZOCKET_HOME=$ZocketHome"
114
72
  Write-Output "web panel: http://127.0.0.1:$WebPort"
115
73
  Write-Output "mcp sse: http://127.0.0.1:$McpPort/sse"
@@ -4,16 +4,15 @@ set -euo pipefail
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
6
 
7
- SOURCE_MODE="${SOURCE_MODE:-auto}" # auto|local|git|pypi
8
- REPO_URL="${REPO_URL:-https://github.com/your-org/zocket.git}"
7
+ SOURCE_MODE="${SOURCE_MODE:-auto}" # auto|local|git|npm
8
+ REPO_URL="${REPO_URL:-https://github.com/aozorin/zocket.git}"
9
9
  REPO_REF="${REPO_REF:-main}"
10
- INSTALL_ROOT="${INSTALL_ROOT:-$HOME/.local/share/zocket}"
11
10
  ZOCKET_HOME_DIR="${ZOCKET_HOME_DIR:-$HOME/.zocket}"
12
- LANGUAGE="${LANGUAGE:-en}" # en|ru
11
+ LANGUAGE="${LANGUAGE:-en}" # en|ru (used by UI; config set via web)
13
12
  WEB_PORT="${WEB_PORT:-18001}"
14
13
  MCP_PORT="${MCP_PORT:-18002}"
15
14
  MCP_STREAM_PORT="${MCP_STREAM_PORT:-18003}"
16
- MCP_MODE="${MCP_MODE:-metadata}" # metadata|admin
15
+ MCP_MODE="${MCP_MODE:-admin}" # metadata|admin
17
16
  AUTOSTART="${AUTOSTART:-user}" # user|system|none
18
17
  SERVICE_USER="${SERVICE_USER:-zocketd}"
19
18
 
@@ -22,10 +21,9 @@ usage() {
22
21
  Usage: $(basename "$0") [options]
23
22
 
24
23
  Options:
25
- --source <auto|local|git|pypi>
24
+ --source <auto|local|git|npm>
26
25
  --repo-url <git-url>
27
26
  --repo-ref <branch-or-tag>
28
- --install-root <path>
29
27
  --zocket-home <path>
30
28
  --lang <en|ru>
31
29
  --web-port <port>
@@ -45,7 +43,6 @@ while [[ $# -gt 0 ]]; do
45
43
  --source) SOURCE_MODE="$2"; shift 2 ;;
46
44
  --repo-url) REPO_URL="$2"; shift 2 ;;
47
45
  --repo-ref) REPO_REF="$2"; shift 2 ;;
48
- --install-root) INSTALL_ROOT="$2"; shift 2 ;;
49
46
  --zocket-home) ZOCKET_HOME_DIR="$2"; shift 2 ;;
50
47
  --lang) LANGUAGE="$2"; shift 2 ;;
51
48
  --web-port) WEB_PORT="$2"; shift 2 ;;
@@ -71,54 +68,100 @@ run_sudo() {
71
68
  fi
72
69
  }
73
70
 
74
- install_python_linux() {
71
+ install_node_linux() {
75
72
  if have_cmd apt-get; then
76
73
  run_sudo apt-get update
77
- run_sudo apt-get install -y python3 python3-venv python3-pip git curl
74
+ run_sudo apt-get install -y nodejs npm git curl
78
75
  return
79
76
  fi
80
77
  if have_cmd dnf; then
81
- run_sudo dnf install -y python3 python3-pip python3-virtualenv git curl
78
+ run_sudo dnf install -y nodejs npm git curl
82
79
  return
83
80
  fi
84
81
  if have_cmd yum; then
85
- run_sudo yum install -y python3 python3-pip git curl
82
+ run_sudo yum install -y nodejs npm git curl
86
83
  return
87
84
  fi
88
85
  if have_cmd pacman; then
89
- run_sudo pacman -Sy --noconfirm python python-pip git curl
86
+ run_sudo pacman -Sy --noconfirm nodejs npm git curl
90
87
  return
91
88
  fi
92
89
  if have_cmd zypper; then
93
- run_sudo zypper install -y python3 python3-pip python3-virtualenv git curl
90
+ run_sudo zypper install -y nodejs npm git curl
94
91
  return
95
92
  fi
96
93
  if have_cmd apk; then
97
- run_sudo apk add --no-cache python3 py3-pip py3-virtualenv git curl
94
+ run_sudo apk add --no-cache nodejs npm git curl
98
95
  return
99
96
  fi
100
- echo "Unsupported Linux package manager. Install Python 3.10+, pip, and venv manually." >&2
97
+ echo "Unsupported Linux package manager. Install Node.js 18+ and npm manually." >&2
101
98
  exit 1
102
99
  }
103
100
 
104
- install_python_macos() {
101
+ install_node_macos() {
105
102
  if ! have_cmd brew; then
106
103
  echo "Homebrew not found. Install Homebrew first: https://brew.sh" >&2
107
104
  exit 1
108
105
  fi
109
- brew install python git curl
106
+ brew install node git curl
110
107
  }
111
108
 
109
+ OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
110
+ if ! have_cmd node || ! have_cmd npm; then
111
+ echo "node/npm not found, installing dependencies..."
112
+ case "$OS" in
113
+ linux*) install_node_linux ;;
114
+ darwin*) install_node_macos ;;
115
+ *)
116
+ echo "Unsupported OS for this installer: $OS" >&2
117
+ exit 1
118
+ ;;
119
+ esac
120
+ fi
121
+
122
+ if [[ "$SOURCE_MODE" == "auto" ]]; then
123
+ if [[ -f "${REPO_ROOT}/package.json" ]]; then
124
+ SOURCE_MODE="local"
125
+ else
126
+ SOURCE_MODE="npm"
127
+ fi
128
+ fi
129
+
130
+ case "$SOURCE_MODE" in
131
+ local)
132
+ npm i -g "${REPO_ROOT}"
133
+ ;;
134
+ git)
135
+ npm i -g "git+${REPO_URL}#${REPO_REF}"
136
+ ;;
137
+ npm)
138
+ npm i -g @ao_zorin/zocket
139
+ ;;
140
+ *)
141
+ echo "Invalid source mode: $SOURCE_MODE" >&2
142
+ exit 2
143
+ ;;
144
+ esac
145
+
146
+ ZOCKET_BIN="$(command -v zocket || true)"
147
+ if [[ -z "${ZOCKET_BIN}" ]]; then
148
+ echo "zocket binary not found after install" >&2
149
+ exit 1
150
+ fi
151
+
152
+ export ZOCKET_HOME="${ZOCKET_HOME_DIR}"
153
+ mkdir -p "${ZOCKET_HOME_DIR}"
154
+ "${ZOCKET_BIN}" init >/dev/null
155
+
112
156
  write_systemd_unit() {
113
157
  local unit_path="$1"
114
158
  local exec_start="$2"
115
159
  local svc_user="$3"
116
160
  local svc_group="$4"
117
161
  local zocket_home="$5"
118
- local extra_rw="${6:-}"
119
162
  run_sudo tee "${unit_path}" >/dev/null <<EOF
120
163
  [Unit]
121
- Description=Zocket $(basename "${unit_path%.service}")
164
+ Description=Zocket service
122
165
  After=network-online.target
123
166
  Wants=network-online.target
124
167
 
@@ -138,7 +181,7 @@ ProtectKernelTunables=true
138
181
  ProtectControlGroups=true
139
182
  LockPersonality=true
140
183
  MemoryDenyWriteExecute=true
141
- ReadWritePaths=${zocket_home} ${extra_rw}
184
+ ReadWritePaths=${zocket_home}
142
185
 
143
186
  [Install]
144
187
  WantedBy=multi-user.target
@@ -152,7 +195,7 @@ write_systemd_user_unit() {
152
195
  mkdir -p "$(dirname "${unit_path}")"
153
196
  cat > "${unit_path}" <<EOF
154
197
  [Unit]
155
- Description=Zocket $(basename "${unit_path%.service}")
198
+ Description=Zocket service
156
199
  After=default.target
157
200
 
158
201
  [Service]
@@ -198,140 +241,33 @@ EOF
198
241
  EOF
199
242
  }
200
243
 
201
- OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
202
- if ! have_cmd python3; then
203
- echo "python3 not found, installing dependencies..."
204
- case "$OS" in
205
- linux*) install_python_linux ;;
206
- darwin*) install_python_macos ;;
207
- *)
208
- echo "Unsupported OS for this installer: $OS" >&2
209
- exit 1
210
- ;;
211
- esac
212
- fi
213
-
214
- if ! have_cmd git; then
215
- echo "git not found, installing..."
216
- case "$OS" in
217
- linux*) install_python_linux ;;
218
- darwin*) install_python_macos ;;
219
- *) echo "Install git manually and rerun." >&2; exit 1 ;;
220
- esac
221
- fi
222
-
223
- if [[ "$SOURCE_MODE" == "auto" ]]; then
224
- if [[ -f "${REPO_ROOT}/pyproject.toml" ]]; then
225
- SOURCE_MODE="local"
226
- else
227
- SOURCE_MODE="git"
228
- fi
229
- fi
230
-
231
- PKG_SOURCE=""
232
- SRC_DIR="${INSTALL_ROOT}/src"
233
-
234
- mkdir -p "${INSTALL_ROOT}"
235
-
236
- if [[ "$SOURCE_MODE" == "local" ]]; then
237
- PKG_SOURCE="${REPO_ROOT}"
238
- elif [[ "$SOURCE_MODE" == "git" ]]; then
239
- if [[ -d "${SRC_DIR}/.git" ]]; then
240
- git -C "${SRC_DIR}" fetch --all --tags
241
- git -C "${SRC_DIR}" checkout "${REPO_REF}"
242
- git -C "${SRC_DIR}" pull --ff-only
243
- else
244
- rm -rf "${SRC_DIR}"
245
- git clone --depth 1 --branch "${REPO_REF}" "${REPO_URL}" "${SRC_DIR}"
246
- fi
247
- PKG_SOURCE="${SRC_DIR}"
248
- elif [[ "$SOURCE_MODE" == "pypi" ]]; then
249
- PKG_SOURCE="zocket"
250
- else
251
- echo "Invalid source mode: $SOURCE_MODE" >&2
252
- exit 2
253
- fi
254
-
255
- VENV_DIR="${INSTALL_ROOT}/venv"
256
- PY_BIN="${VENV_DIR}/bin/python3"
257
- ZOCKET_BIN="${VENV_DIR}/bin/zocket"
258
-
259
- python3 -m venv "${VENV_DIR}"
260
- "${PY_BIN}" -m pip install --upgrade pip setuptools wheel
261
-
262
- if [[ "$SOURCE_MODE" == "pypi" ]]; then
263
- "${PY_BIN}" -m pip install --upgrade "${PKG_SOURCE}"
264
- else
265
- "${PY_BIN}" -m pip install --upgrade "${PKG_SOURCE}"
266
- fi
267
-
268
- mkdir -p "$HOME/.local/bin"
269
- ln -sf "${ZOCKET_BIN}" "$HOME/.local/bin/zocket"
270
-
271
- export ZOCKET_HOME="${ZOCKET_HOME_DIR}"
272
- mkdir -p "${ZOCKET_HOME_DIR}"
273
-
274
- if [[ ! -f "${ZOCKET_HOME_DIR}/vault.enc" ]]; then
275
- "${ZOCKET_BIN}" init
276
- fi
277
-
278
- "${ZOCKET_BIN}" config set-language "${LANGUAGE}" >/dev/null
244
+ 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}"
279
245
 
280
246
  if [[ "$AUTOSTART" == "user" && "$OS" == linux* ]]; then
281
247
  USER_UNIT_DIR="$HOME/.config/systemd/user"
282
- write_systemd_user_unit "${USER_UNIT_DIR}/zocket-web.service" \
283
- "${PY_BIN} -m zocket web --host 127.0.0.1 --port ${WEB_PORT}" \
284
- "${ZOCKET_HOME_DIR}"
285
- write_systemd_user_unit "${USER_UNIT_DIR}/zocket-mcp-sse.service" \
286
- "${PY_BIN} -m zocket mcp --transport sse --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_PORT}" \
287
- "${ZOCKET_HOME_DIR}"
288
- write_systemd_user_unit "${USER_UNIT_DIR}/zocket-mcp-http.service" \
289
- "${PY_BIN} -m zocket mcp --transport streamable-http --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_STREAM_PORT}" \
290
- "${ZOCKET_HOME_DIR}"
248
+ write_systemd_user_unit "${USER_UNIT_DIR}/zocket.service" "${EXEC_START}" "${ZOCKET_HOME_DIR}"
291
249
  systemctl --user daemon-reload
292
- systemctl --user enable --now zocket-web.service zocket-mcp-sse.service zocket-mcp-http.service >/dev/null
250
+ systemctl --user enable --now zocket.service >/dev/null
293
251
  fi
294
252
 
295
253
  if [[ "$AUTOSTART" == "system" && "$OS" == linux* ]]; then
296
- write_systemd_unit "/etc/systemd/system/zocket-web.service" \
297
- "${PY_BIN} -m zocket web --host 127.0.0.1 --port ${WEB_PORT}" \
298
- "${SERVICE_USER}" "${SERVICE_USER}" "${ZOCKET_HOME_DIR}" "/tmp"
299
- write_systemd_unit "/etc/systemd/system/zocket-mcp-sse.service" \
300
- "${PY_BIN} -m zocket mcp --transport sse --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_PORT}" \
301
- "${SERVICE_USER}" "${SERVICE_USER}" "${ZOCKET_HOME_DIR}"
302
- write_systemd_unit "/etc/systemd/system/zocket-mcp-http.service" \
303
- "${PY_BIN} -m zocket mcp --transport streamable-http --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_STREAM_PORT}" \
304
- "${SERVICE_USER}" "${SERVICE_USER}" "${ZOCKET_HOME_DIR}"
254
+ write_systemd_unit "/etc/systemd/system/zocket.service" "${EXEC_START}" "${SERVICE_USER}" "${SERVICE_USER}" "${ZOCKET_HOME_DIR}"
305
255
  run_sudo systemctl daemon-reload
306
- run_sudo systemctl enable --now zocket-web.service zocket-mcp-sse.service zocket-mcp-http.service >/dev/null
256
+ run_sudo systemctl enable --now zocket.service >/dev/null
307
257
  fi
308
258
 
309
259
  if [[ "$AUTOSTART" != "none" && "$OS" == darwin* ]]; then
310
260
  PLIST_DIR="$HOME/Library/LaunchAgents"
311
- install_launchd "dev.zocket.web" \
312
- "${PY_BIN} -m zocket web --host 127.0.0.1 --port ${WEB_PORT}" \
313
- "${ZOCKET_HOME_DIR}" "${PLIST_DIR}"
314
- install_launchd "dev.zocket.mcp-sse" \
315
- "${PY_BIN} -m zocket mcp --transport sse --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_PORT}" \
316
- "${ZOCKET_HOME_DIR}" "${PLIST_DIR}"
317
- install_launchd "dev.zocket.mcp-streamable" \
318
- "${PY_BIN} -m zocket mcp --transport streamable-http --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_STREAM_PORT}" \
319
- "${ZOCKET_HOME_DIR}" "${PLIST_DIR}"
320
- launchctl unload "${PLIST_DIR}/dev.zocket.web.plist" >/dev/null 2>&1 || true
321
- launchctl unload "${PLIST_DIR}/dev.zocket.mcp-sse.plist" >/dev/null 2>&1 || true
322
- launchctl unload "${PLIST_DIR}/dev.zocket.mcp-streamable.plist" >/dev/null 2>&1 || true
323
- launchctl load "${PLIST_DIR}/dev.zocket.web.plist"
324
- launchctl load "${PLIST_DIR}/dev.zocket.mcp-sse.plist"
325
- launchctl load "${PLIST_DIR}/dev.zocket.mcp-streamable.plist"
261
+ install_launchd "dev.zocket" "${EXEC_START}" "${ZOCKET_HOME_DIR}" "${PLIST_DIR}"
262
+ launchctl unload "${PLIST_DIR}/dev.zocket.plist" >/dev/null 2>&1 || true
263
+ launchctl load "${PLIST_DIR}/dev.zocket.plist"
326
264
  fi
327
265
 
328
266
  cat <<EOF
329
267
  zocket installed successfully.
330
268
 
331
- Runtime:
332
- venv: ${VENV_DIR}
333
- zocket: ${ZOCKET_BIN}
334
- ZOCKET_HOME=${ZOCKET_HOME_DIR}
269
+ zocket: ${ZOCKET_BIN}
270
+ ZOCKET_HOME=${ZOCKET_HOME_DIR}
335
271
 
336
272
  Default ports:
337
273
  web panel: http://127.0.0.1:${WEB_PORT}
@@ -339,8 +275,5 @@ Default ports:
339
275
  MCP HTTP: http://127.0.0.1:${MCP_STREAM_PORT}/mcp
340
276
 
341
277
  Next steps:
342
- 1) Open web: ${ZOCKET_BIN} web --host 127.0.0.1 --port ${WEB_PORT}
343
- 2) MCP SSE (Claude Code): ${ZOCKET_BIN} mcp --transport sse --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_PORT}
344
- 3) MCP Streamable (Codex): ${ZOCKET_BIN} mcp --transport streamable-http --mode ${MCP_MODE} --host 127.0.0.1 --port ${MCP_STREAM_PORT}
345
- 4) MCP stdio: ${ZOCKET_BIN} mcp --transport stdio --mode ${MCP_MODE}
278
+ 1) Start now: ${EXEC_START}
346
279
  EOF