zelari-code 2.20.0 → 2.22.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.
@@ -44725,7 +44725,10 @@ var init_semantic2 = __esm({
44725
44725
  // src/cli/provider/openai-compatible.ts
44726
44726
  var openai_compatible_exports = {};
44727
44727
  __export(openai_compatible_exports, {
44728
+ PROVIDER_CONNECT_TIMEOUT_MS: () => PROVIDER_CONNECT_TIMEOUT_MS,
44728
44729
  PROVIDER_ENDPOINTS: () => PROVIDER_ENDPOINTS,
44730
+ PROVIDER_STREAM_IDLE_MS: () => PROVIDER_STREAM_IDLE_MS,
44731
+ PROVIDER_STREAM_MAX_MS: () => PROVIDER_STREAM_MAX_MS,
44729
44732
  dataUriFromImage: () => dataUriFromImage,
44730
44733
  modelSupportsVision: () => modelSupportsVision,
44731
44734
  openaiCompatibleProvider: () => openaiCompatibleProvider,
@@ -46760,14 +46763,27 @@ function anthropicMessagesProvider(config2) {
46760
46763
  const base2 = config2.baseUrl.replace(/\/$/, "").replace(/\/v1$/, "");
46761
46764
  const url2 = `${base2}/v1/messages`;
46762
46765
  let response;
46766
+ const connectController = new AbortController();
46767
+ const connectTimer = setTimeout(
46768
+ () => connectController.abort(
46769
+ new Error(
46770
+ `Provider connect timeout after ${Math.round(PROVIDER_CONNECT_TIMEOUT_MS / 1e3)}s (no response headers). Override ZELARI_PROVIDER_CONNECT_TIMEOUT_MS.`
46771
+ )
46772
+ ),
46773
+ PROVIDER_CONNECT_TIMEOUT_MS
46774
+ );
46775
+ const signals = [connectController.signal];
46776
+ if (params.signal) signals.push(params.signal);
46763
46777
  try {
46764
46778
  response = await fetch(url2, {
46765
46779
  method: "POST",
46766
46780
  headers: authHeaders(config2.apiKey, ttlPref === "1h"),
46767
46781
  body: JSON.stringify(body),
46768
- signal: params.signal
46782
+ signal: signals.length === 1 ? signals[0] : AbortSignal.any(signals)
46769
46783
  });
46784
+ clearTimeout(connectTimer);
46770
46785
  } catch (err) {
46786
+ clearTimeout(connectTimer);
46771
46787
  yield {
46772
46788
  kind: "error",
46773
46789
  message: `Network error: ${err instanceof Error ? err.message : String(err)}`
@@ -46802,9 +46818,17 @@ function anthropicMessagesProvider(config2) {
46802
46818
  };
46803
46819
  currentTool = null;
46804
46820
  };
46821
+ const streamStartedAt = Date.now();
46822
+ let lastUsefulAt = streamStartedAt;
46823
+ const streamDeadline = streamStartedAt + PROVIDER_STREAM_MAX_MS;
46805
46824
  try {
46806
46825
  while (true) {
46807
- const { value, done } = await reader.read();
46826
+ const { value, done } = await readChunkWithTimeout(reader, {
46827
+ idleMs: PROVIDER_STREAM_IDLE_MS,
46828
+ deadlineMs: streamDeadline,
46829
+ signal: params.signal,
46830
+ lastUsefulAt: () => lastUsefulAt
46831
+ });
46808
46832
  if (done) break;
46809
46833
  buffer += decoder.decode(value, { stream: true });
46810
46834
  const lines = buffer.split("\n");
@@ -46821,6 +46845,7 @@ function anthropicMessagesProvider(config2) {
46821
46845
  continue;
46822
46846
  }
46823
46847
  const type = ev.type;
46848
+ if (type !== "ping") lastUsefulAt = Date.now();
46824
46849
  if (type === "content_block_delta") {
46825
46850
  const delta = ev.delta;
46826
46851
  if (delta?.type === "text_delta" && typeof delta.text === "string") {
@@ -46894,6 +46919,7 @@ var ANTHROPIC_VERSION, ANTHROPIC_BETA, ANTHROPIC_BETA_EXTENDED_CACHE_TTL;
46894
46919
  var init_anthropic = __esm({
46895
46920
  "src/cli/provider/anthropic.ts"() {
46896
46921
  "use strict";
46922
+ init_openai_compatible();
46897
46923
  init_chatStats();
46898
46924
  init_thinking();
46899
46925
  ANTHROPIC_VERSION = "2023-06-01";
@@ -46972,14 +46998,27 @@ function chatgptResponsesProvider(config2) {
46972
46998
  const base2 = config2.baseUrl.replace(/\/$/, "");
46973
46999
  const url2 = `${base2}/responses`;
46974
47000
  let response;
47001
+ const connectController = new AbortController();
47002
+ const connectTimer = setTimeout(
47003
+ () => connectController.abort(
47004
+ new Error(
47005
+ `Provider connect timeout after ${Math.round(PROVIDER_CONNECT_TIMEOUT_MS / 1e3)}s (no response headers). Override ZELARI_PROVIDER_CONNECT_TIMEOUT_MS.`
47006
+ )
47007
+ ),
47008
+ PROVIDER_CONNECT_TIMEOUT_MS
47009
+ );
47010
+ const signals = [connectController.signal];
47011
+ if (params.signal) signals.push(params.signal);
46975
47012
  try {
46976
47013
  response = await fetch(url2, {
46977
47014
  method: "POST",
46978
47015
  headers: headers(config2),
46979
47016
  body: JSON.stringify(body),
46980
- signal: params.signal
47017
+ signal: signals.length === 1 ? signals[0] : AbortSignal.any(signals)
46981
47018
  });
47019
+ clearTimeout(connectTimer);
46982
47020
  } catch (err) {
47021
+ clearTimeout(connectTimer);
46983
47022
  yield {
46984
47023
  kind: "error",
46985
47024
  message: `Network error: ${err instanceof Error ? err.message : String(err)}`
@@ -47009,9 +47048,17 @@ function chatgptResponsesProvider(config2) {
47009
47048
  emittedTool = true;
47010
47049
  yield { kind: "tool_call", toolCallId: t.id, toolName: t.name, args };
47011
47050
  };
47051
+ const streamStartedAt = Date.now();
47052
+ let lastUsefulAt = streamStartedAt;
47053
+ const streamDeadline = streamStartedAt + PROVIDER_STREAM_MAX_MS;
47012
47054
  try {
47013
47055
  while (true) {
47014
- const { value, done } = await reader.read();
47056
+ const { value, done } = await readChunkWithTimeout(reader, {
47057
+ idleMs: PROVIDER_STREAM_IDLE_MS,
47058
+ deadlineMs: streamDeadline,
47059
+ signal: params.signal,
47060
+ lastUsefulAt: () => lastUsefulAt
47061
+ });
47015
47062
  if (done) break;
47016
47063
  buffer += decoder.decode(value, { stream: true });
47017
47064
  const lines = buffer.split("\n");
@@ -47028,6 +47075,7 @@ function chatgptResponsesProvider(config2) {
47028
47075
  continue;
47029
47076
  }
47030
47077
  const type = typeof ev.type === "string" ? ev.type : "";
47078
+ if (type) lastUsefulAt = Date.now();
47031
47079
  if (type === "response.output_text.delta" && typeof ev.delta === "string") {
47032
47080
  yield { kind: "text", delta: ev.delta };
47033
47081
  } else if (type === "response.reasoning_text.delta" && typeof ev.delta === "string") {
@@ -47083,6 +47131,7 @@ function chatgptResponsesProvider(config2) {
47083
47131
  var init_chatgpt = __esm({
47084
47132
  "src/cli/provider/chatgpt.ts"() {
47085
47133
  "use strict";
47134
+ init_openai_compatible();
47086
47135
  init_thinking();
47087
47136
  }
47088
47137
  });
@@ -55304,39 +55353,224 @@ var init_toolRegistry2 = __esm({
55304
55353
  }
55305
55354
  });
55306
55355
 
55356
+ // src/cli/mcp/httpTransport.ts
55357
+ var SSE_DATA_RE, ABORT_GRACE_MS, McpHttpTransport;
55358
+ var init_httpTransport = __esm({
55359
+ "src/cli/mcp/httpTransport.ts"() {
55360
+ "use strict";
55361
+ SSE_DATA_RE = /^data:\s?(.*)$/;
55362
+ ABORT_GRACE_MS = 250;
55363
+ McpHttpTransport = class {
55364
+ constructor(opts) {
55365
+ this.opts = opts;
55366
+ }
55367
+ sessionId = null;
55368
+ closed = false;
55369
+ reinit = null;
55370
+ controllers = /* @__PURE__ */ new Set();
55371
+ get hasSession() {
55372
+ return this.sessionId !== null;
55373
+ }
55374
+ /**
55375
+ * Deliver one JSON-RPC message. Resolves once the response (if any) has
55376
+ * been fed to onMessage. Transport-level failures are converted into
55377
+ * JSON-RPC error responses so the client's pending map rejects cleanly
55378
+ * through the same pump used for stdio.
55379
+ */
55380
+ async send(msg, timeoutMs2) {
55381
+ if (this.closed) throw new Error(`[mcp:${this.opts.serverName}] transport closed`);
55382
+ try {
55383
+ await this.post(msg, timeoutMs2, false);
55384
+ } catch (err) {
55385
+ if (msg.id !== void 0) {
55386
+ this.opts.onMessage({
55387
+ jsonrpc: "2.0",
55388
+ id: msg.id,
55389
+ error: {
55390
+ code: -32e3,
55391
+ message: err instanceof Error ? err.message : String(err)
55392
+ }
55393
+ });
55394
+ }
55395
+ }
55396
+ }
55397
+ /** Best-effort session teardown (HTTP DELETE), then abort in-flight POSTs. */
55398
+ close() {
55399
+ this.closed = true;
55400
+ for (const ac of this.controllers) ac.abort();
55401
+ this.controllers.clear();
55402
+ const sid = this.sessionId;
55403
+ this.sessionId = null;
55404
+ if (!sid) return;
55405
+ const headers2 = {
55406
+ ...this.opts.headers ?? {},
55407
+ "mcp-session-id": sid
55408
+ };
55409
+ void fetch(this.opts.url, { method: "DELETE", headers: headers2 }).catch(() => {
55410
+ });
55411
+ }
55412
+ // ── internals ────────────────────────────────────────────────────────
55413
+ async post(msg, timeoutMs2, replayed) {
55414
+ const ac = new AbortController();
55415
+ this.controllers.add(ac);
55416
+ const timer = setTimeout(() => ac.abort(), timeoutMs2 + ABORT_GRACE_MS);
55417
+ const hadSession = this.sessionId !== null;
55418
+ try {
55419
+ const headers2 = {
55420
+ "content-type": "application/json",
55421
+ accept: "application/json, text/event-stream",
55422
+ ...this.opts.headers ?? {}
55423
+ };
55424
+ if (this.sessionId) headers2["mcp-session-id"] = this.sessionId;
55425
+ const res = await fetch(this.opts.url, {
55426
+ method: "POST",
55427
+ headers: headers2,
55428
+ signal: ac.signal,
55429
+ body: JSON.stringify({ jsonrpc: "2.0", ...msg })
55430
+ });
55431
+ const sid = res.headers.get("mcp-session-id");
55432
+ if (sid) this.sessionId = sid;
55433
+ if (res.status === 404 && hadSession && !replayed && msg.id !== void 0) {
55434
+ this.sessionId = null;
55435
+ await this.ensureSession();
55436
+ return this.post(msg, timeoutMs2, true);
55437
+ }
55438
+ if (!res.ok && res.status !== 202) {
55439
+ throw new Error(`HTTP ${res.status} ${res.statusText}`.trim());
55440
+ }
55441
+ if (msg.id === void 0) {
55442
+ await res.body?.cancel();
55443
+ return;
55444
+ }
55445
+ const contentType = res.headers.get("content-type") ?? "";
55446
+ if (contentType.includes("text/event-stream")) {
55447
+ await this.readSse(res);
55448
+ } else {
55449
+ const body = await res.text();
55450
+ if (body) this.opts.onMessage(JSON.parse(body));
55451
+ }
55452
+ } finally {
55453
+ clearTimeout(timer);
55454
+ this.controllers.delete(ac);
55455
+ }
55456
+ }
55457
+ /** Dedupe concurrent session-loss recoveries into one handshake. */
55458
+ async ensureSession() {
55459
+ if (!this.reinit) {
55460
+ this.reinit = this.opts.onSessionLost().finally(() => {
55461
+ this.reinit = null;
55462
+ }).catch(() => {
55463
+ });
55464
+ }
55465
+ await this.reinit;
55466
+ }
55467
+ /** Minimal SSE reader: dispatch complete `data:` events to onMessage. */
55468
+ async readSse(res) {
55469
+ const reader = res.body?.getReader();
55470
+ if (!reader) return;
55471
+ const decoder = new TextDecoder();
55472
+ let buf = "";
55473
+ let data = "";
55474
+ const dispatch = () => {
55475
+ const payload = data.trim();
55476
+ data = "";
55477
+ if (!payload) return;
55478
+ try {
55479
+ this.opts.onMessage(JSON.parse(payload));
55480
+ } catch {
55481
+ }
55482
+ };
55483
+ for (; ; ) {
55484
+ const { done, value } = await reader.read();
55485
+ if (done) break;
55486
+ buf += decoder.decode(value, { stream: true });
55487
+ let nl;
55488
+ while ((nl = buf.indexOf("\n")) >= 0) {
55489
+ const line = buf.slice(0, nl).replace(/\r$/, "");
55490
+ buf = buf.slice(nl + 1);
55491
+ if (line === "") {
55492
+ dispatch();
55493
+ continue;
55494
+ }
55495
+ const m = SSE_DATA_RE.exec(line);
55496
+ if (m) data += (data ? "\n" : "") + m[1];
55497
+ }
55498
+ }
55499
+ dispatch();
55500
+ }
55501
+ };
55502
+ }
55503
+ });
55504
+
55307
55505
  // src/cli/mcp/mcpClient.ts
55308
55506
  import { spawn as spawn14 } from "node:child_process";
55309
- var DEFAULT_REQUEST_TIMEOUT_MS, INIT_TIMEOUT_MS, MCP_PROTOCOL_VERSION, McpClient;
55507
+ var DEFAULT_REQUEST_TIMEOUT_MS, INIT_TIMEOUT_MS, MAX_LIST_PAGES, MCP_PROTOCOL_VERSION, McpClient;
55310
55508
  var init_mcpClient = __esm({
55311
55509
  "src/cli/mcp/mcpClient.ts"() {
55312
55510
  "use strict";
55313
55511
  init_cmdline();
55314
55512
  init_updater();
55513
+ init_httpTransport();
55315
55514
  DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
55316
55515
  INIT_TIMEOUT_MS = 15e3;
55516
+ MAX_LIST_PAGES = 50;
55317
55517
  MCP_PROTOCOL_VERSION = "2025-03-26";
55318
55518
  McpClient = class {
55319
55519
  constructor(serverName, config2) {
55320
55520
  this.serverName = serverName;
55321
55521
  this.config = config2;
55522
+ this.defaultTimeoutMs = config2.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
55523
+ this.serial = config2.serial ?? this.transportKind() === "http";
55322
55524
  }
55323
55525
  child = null;
55526
+ transport = null;
55324
55527
  nextId = 1;
55325
55528
  pending = /* @__PURE__ */ new Map();
55326
55529
  stdoutBuffer = "";
55327
55530
  closed = false;
55328
- /** Spawn the server process and run the MCP initialize handshake. */
55531
+ /** Tail of the serial queue (config.serial); requests run one at a time. */
55532
+ queueTail = Promise.resolve();
55533
+ /** True while the 404-recovery handshake runs: bypasses the serial queue
55534
+ * (the queued request that triggered the 404 is waiting on this handshake). */
55535
+ recovering = false;
55536
+ serial;
55537
+ defaultTimeoutMs;
55538
+ transportKind() {
55539
+ if (this.config.type) return this.config.type;
55540
+ return this.config.url && !this.config.command ? "http" : "stdio";
55541
+ }
55542
+ /** Connect (spawn / HTTP session) and run the MCP initialize handshake. */
55329
55543
  async start() {
55330
- if (this.child) return;
55544
+ if (this.child || this.transport) return;
55545
+ if (this.transportKind() === "http") {
55546
+ const url2 = this.config.url;
55547
+ if (!url2) {
55548
+ throw new Error(`[mcp:${this.serverName}] http server requires a url`);
55549
+ }
55550
+ this.transport = new McpHttpTransport({
55551
+ serverName: this.serverName,
55552
+ url: url2,
55553
+ // env may carry an explicit Authorization header for remote servers.
55554
+ headers: this.config.env?.AUTHORIZATION ? { Authorization: this.config.env.AUTHORIZATION } : void 0,
55555
+ onMessage: (msg) => this.handleMessage(msg),
55556
+ onSessionLost: () => this.handshake()
55557
+ });
55558
+ await this.handshake();
55559
+ return;
55560
+ }
55561
+ const command = this.config.command;
55562
+ if (!command) {
55563
+ throw new Error(`[mcp:${this.serverName}] stdio server requires a command`);
55564
+ }
55331
55565
  const spawnOpts = {
55332
55566
  stdio: ["pipe", "pipe", "pipe"],
55333
55567
  env: { ...process.env, ...this.config.env ?? {} },
55334
55568
  windowsHide: true
55335
55569
  };
55336
- const child = process.platform === "win32" ? spawn14(buildCmdLine(this.config.command, this.config.args ?? []), {
55570
+ const child = process.platform === "win32" ? spawn14(buildCmdLine(command, this.config.args ?? []), {
55337
55571
  ...spawnOpts,
55338
55572
  shell: true
55339
- }) : spawn14(this.config.command, this.config.args ?? [], spawnOpts);
55573
+ }) : spawn14(command, this.config.args ?? [], spawnOpts);
55340
55574
  this.child = child;
55341
55575
  child.stdout.setEncoding("utf8");
55342
55576
  child.stdout.on("data", (chunk) => this.onStdout(chunk));
@@ -55355,37 +55589,60 @@ var init_mcpClient = __esm({
55355
55589
  );
55356
55590
  }
55357
55591
  });
55358
- await this.request(
55359
- "initialize",
55360
- {
55361
- protocolVersion: MCP_PROTOCOL_VERSION,
55362
- capabilities: {},
55363
- clientInfo: { name: "zelari-code", version: getCurrentVersion() }
55364
- },
55365
- INIT_TIMEOUT_MS
55366
- );
55367
- this.notify("notifications/initialized", {});
55592
+ await this.handshake();
55368
55593
  }
55369
- /** Discover the server's tools. */
55594
+ async handshake() {
55595
+ const prev2 = this.recovering;
55596
+ this.recovering = true;
55597
+ try {
55598
+ await this.request(
55599
+ "initialize",
55600
+ {
55601
+ protocolVersion: MCP_PROTOCOL_VERSION,
55602
+ capabilities: {},
55603
+ clientInfo: { name: "zelari-code", version: getCurrentVersion() }
55604
+ },
55605
+ INIT_TIMEOUT_MS
55606
+ );
55607
+ this.notify("notifications/initialized", {});
55608
+ } finally {
55609
+ this.recovering = prev2;
55610
+ }
55611
+ }
55612
+ /**
55613
+ * Discover the server's tools. Follows `nextCursor` pagination so
55614
+ * large servers (hundreds of tools) are listed completely.
55615
+ */
55370
55616
  async listTools() {
55371
- const res = await this.request("tools/list", {});
55372
- return (res.tools ?? []).filter(
55373
- (t) => !!t.name
55374
- ).map((t) => ({
55375
- name: t.name,
55376
- description: t.description ?? "",
55377
- inputSchema: t.inputSchema ?? { type: "object", properties: {} }
55378
- }));
55617
+ const tools = [];
55618
+ let cursor;
55619
+ for (let page = 0; page < MAX_LIST_PAGES; page++) {
55620
+ const res = await this.request(
55621
+ "tools/list",
55622
+ cursor ? { cursor } : {}
55623
+ );
55624
+ for (const t of res.tools ?? []) {
55625
+ if (!t.name) continue;
55626
+ tools.push({
55627
+ name: t.name,
55628
+ description: t.description ?? "",
55629
+ inputSchema: t.inputSchema ?? { type: "object", properties: {} }
55630
+ });
55631
+ }
55632
+ cursor = typeof res.nextCursor === "string" ? res.nextCursor : void 0;
55633
+ if (!cursor) break;
55634
+ }
55635
+ return tools;
55379
55636
  }
55380
55637
  /**
55381
55638
  * Call a tool. Returns the concatenated text content; non-text content
55382
55639
  * items are summarized by type. Throws when the server flags isError.
55383
55640
  */
55384
- async callTool(name, args, timeoutMs2 = DEFAULT_REQUEST_TIMEOUT_MS) {
55641
+ async callTool(name, args, timeoutMs2) {
55385
55642
  const res = await this.request(
55386
55643
  "tools/call",
55387
55644
  { name, arguments: args },
55388
- timeoutMs2
55645
+ timeoutMs2 ?? this.defaultTimeoutMs
55389
55646
  );
55390
55647
  const text = (res.content ?? []).map(
55391
55648
  (c) => c.type === "text" && typeof c.text === "string" ? c.text : `[${c.type ?? "unknown"} content]`
@@ -55394,20 +55651,39 @@ var init_mcpClient = __esm({
55394
55651
  throw new Error(text || `tool "${name}" reported an error`);
55395
55652
  return text;
55396
55653
  }
55397
- /** Terminate the server process and reject all in-flight requests. */
55654
+ /** Terminate the server / session and reject all in-flight requests. */
55398
55655
  close() {
55399
55656
  this.closed = true;
55400
55657
  this.failAll(new Error(`[mcp:${this.serverName}] client closed`));
55658
+ if (this.transport) {
55659
+ this.transport.close();
55660
+ this.transport = null;
55661
+ return;
55662
+ }
55401
55663
  this.child?.kill();
55402
55664
  this.child = null;
55403
55665
  }
55404
- // ── JSON-RPC plumbing ────────────────────────────────────────────────
55405
- request(method, params, timeoutMs2 = DEFAULT_REQUEST_TIMEOUT_MS) {
55666
+ // ── JSON-RPC plumbing (shared by both transports) ────────────────────
55667
+ request(method, params, timeoutMs2) {
55668
+ const effective = timeoutMs2 ?? this.defaultTimeoutMs;
55669
+ if (!this.serial || this.recovering)
55670
+ return this.dispatch(method, params, effective);
55671
+ const run = this.queueTail.then(
55672
+ () => this.dispatch(method, params, effective),
55673
+ () => this.dispatch(method, params, effective)
55674
+ );
55675
+ this.queueTail = run.then(
55676
+ () => void 0,
55677
+ () => void 0
55678
+ );
55679
+ return run;
55680
+ }
55681
+ dispatch(method, params, timeoutMs2) {
55682
+ const transport = this.transport;
55406
55683
  const child = this.child;
55407
- if (!child)
55684
+ if (!transport && !child)
55408
55685
  return Promise.reject(new Error(`[mcp:${this.serverName}] not started`));
55409
55686
  const id3 = this.nextId++;
55410
- const payload = JSON.stringify({ jsonrpc: "2.0", id: id3, method, params });
55411
55687
  return new Promise((resolve9, reject) => {
55412
55688
  const timer = setTimeout(() => {
55413
55689
  this.pending.delete(id3);
@@ -55418,16 +55694,25 @@ var init_mcpClient = __esm({
55418
55694
  );
55419
55695
  }, timeoutMs2);
55420
55696
  this.pending.set(id3, { resolve: resolve9, reject, timer });
55421
- child.stdin.write(payload + "\n", (err) => {
55422
- if (err) {
55423
- clearTimeout(timer);
55424
- this.pending.delete(id3);
55425
- reject(err);
55426
- }
55427
- });
55697
+ if (transport) {
55698
+ void transport.send({ id: id3, method, params }, timeoutMs2);
55699
+ } else {
55700
+ const payload = JSON.stringify({ jsonrpc: "2.0", id: id3, method, params });
55701
+ child.stdin.write(payload + "\n", (err) => {
55702
+ if (err) {
55703
+ clearTimeout(timer);
55704
+ this.pending.delete(id3);
55705
+ reject(err);
55706
+ }
55707
+ });
55708
+ }
55428
55709
  });
55429
55710
  }
55430
55711
  notify(method, params) {
55712
+ if (this.transport) {
55713
+ void this.transport.send({ method, params }, this.defaultTimeoutMs);
55714
+ return;
55715
+ }
55431
55716
  this.child?.stdin.write(
55432
55717
  JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n"
55433
55718
  );
@@ -55439,26 +55724,29 @@ var init_mcpClient = __esm({
55439
55724
  const line = this.stdoutBuffer.slice(0, nl).trim();
55440
55725
  this.stdoutBuffer = this.stdoutBuffer.slice(nl + 1);
55441
55726
  if (!line) continue;
55442
- let msg;
55443
55727
  try {
55444
- msg = JSON.parse(line);
55728
+ this.handleMessage(JSON.parse(line));
55445
55729
  } catch {
55446
55730
  continue;
55447
55731
  }
55448
- if (typeof msg.id !== "number") continue;
55449
- const pending = this.pending.get(msg.id);
55450
- if (!pending) continue;
55451
- this.pending.delete(msg.id);
55452
- clearTimeout(pending.timer);
55453
- if (msg.error) {
55454
- pending.reject(
55455
- new Error(
55456
- `[mcp:${this.serverName}] ${msg.error.message ?? "JSON-RPC error"} (code ${msg.error.code ?? "?"})`
55457
- )
55458
- );
55459
- } else {
55460
- pending.resolve(msg.result);
55461
- }
55732
+ }
55733
+ }
55734
+ /** Route one parsed JSON-RPC message to its pending request (both transports). */
55735
+ handleMessage(msg) {
55736
+ const m = msg;
55737
+ if (typeof m.id !== "number") return;
55738
+ const pending = this.pending.get(m.id);
55739
+ if (!pending) return;
55740
+ this.pending.delete(m.id);
55741
+ clearTimeout(pending.timer);
55742
+ if (m.error) {
55743
+ pending.reject(
55744
+ new Error(
55745
+ `[mcp:${this.serverName}] ${m.error.message ?? "JSON-RPC error"} (code ${m.error.code ?? "?"})`
55746
+ )
55747
+ );
55748
+ } else {
55749
+ pending.resolve(m.result);
55462
55750
  }
55463
55751
  }
55464
55752
  failAll(err) {
@@ -55493,11 +55781,17 @@ function readFile6(path91) {
55493
55781
  const parsed = JSON.parse(readFileSync29(path91, "utf8"));
55494
55782
  const out = {};
55495
55783
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
55496
- if (!cfg || typeof cfg.command !== "string" || !cfg.command.trim()) continue;
55784
+ const hasCommand = !!cfg && typeof cfg.command === "string" && !!cfg.command.trim();
55785
+ const hasUrl = !!cfg && typeof cfg.url === "string" && /^https?:\/\//i.test(cfg.url);
55786
+ if (!cfg || !hasCommand && !hasUrl) continue;
55497
55787
  out[name] = {
55498
- command: cfg.command.trim(),
55788
+ command: hasCommand ? cfg.command.trim() : void 0,
55499
55789
  args: Array.isArray(cfg.args) ? cfg.args.map(String) : void 0,
55500
55790
  env: cfg.env && typeof cfg.env === "object" ? cfg.env : void 0,
55791
+ type: hasUrl && !hasCommand ? "http" : cfg.type === "http" ? "http" : "stdio",
55792
+ url: hasUrl ? cfg.url.trim() : void 0,
55793
+ timeoutMs: typeof cfg.timeoutMs === "number" && cfg.timeoutMs > 0 ? cfg.timeoutMs : void 0,
55794
+ serial: typeof cfg.serial === "boolean" ? cfg.serial : void 0,
55501
55795
  enabled: cfg.enabled !== false
55502
55796
  };
55503
55797
  }
@@ -55539,8 +55833,13 @@ function upsertMcpServer(opts) {
55539
55833
  error: "Invalid server name (use letters, digits, _ -)"
55540
55834
  };
55541
55835
  }
55542
- if (!opts.config.command?.trim()) {
55543
- return { ok: false, error: "command is required" };
55836
+ const hasCommand = !!opts.config.command?.trim();
55837
+ const hasUrl = typeof opts.config.url === "string" && /^https?:\/\//i.test(opts.config.url);
55838
+ if (!hasCommand && !hasUrl) {
55839
+ return {
55840
+ ok: false,
55841
+ error: "either command (stdio) or url (http) is required"
55842
+ };
55544
55843
  }
55545
55844
  let path91;
55546
55845
  if (opts.scope === "user") {
@@ -55557,9 +55856,13 @@ function upsertMcpServer(opts) {
55557
55856
  }
55558
55857
  const current = readFile6(path91);
55559
55858
  current[name] = {
55560
- command: opts.config.command.trim(),
55859
+ command: hasCommand ? opts.config.command.trim() : void 0,
55561
55860
  args: opts.config.args,
55562
55861
  env: opts.config.env,
55862
+ type: hasUrl ? "http" : opts.config.type === "http" ? "http" : "stdio",
55863
+ url: hasUrl ? opts.config.url.trim() : void 0,
55864
+ timeoutMs: opts.config.timeoutMs,
55865
+ serial: opts.config.serial,
55563
55866
  enabled: opts.config.enabled !== false
55564
55867
  };
55565
55868
  writeFile2(path91, current);
@@ -55634,9 +55937,35 @@ function buildQwenMmPreset() {
55634
55937
  ]
55635
55938
  };
55636
55939
  }
55940
+ function buildUnrealPreset() {
55941
+ const url2 = process.env.UNREAL_MCP_URL?.trim() || "http://127.0.0.1:8000/mcp";
55942
+ return {
55943
+ id: "unreal-mcp",
55944
+ servers: {
55945
+ "unreal-mcp": {
55946
+ type: "http",
55947
+ url: url2,
55948
+ // Editor tool calls (asset scans, builds, PIE) can be slow.
55949
+ timeoutMs: 12e4,
55950
+ // Epic guidance: never overlap calls on the editor's game thread.
55951
+ serial: true,
55952
+ enabled: true
55953
+ }
55954
+ },
55955
+ notes: [
55956
+ "Unreal Engine 5.8+ \u2014 MCP server embedded in the editor (Experimental feature).",
55957
+ "Editor: enable the 'Model Context Protocol' plugin, then Edit \u2192 Project Settings \u2192 Plugins \u2192 MCP Server.",
55958
+ `Endpoint: ${url2} (override with UNREAL_MCP_URL; port/path configurable in the editor).`,
55959
+ "Tool Search ON by default: tools surface as list_toolsets / describe_toolset / call_tool.",
55960
+ "Requests are serialized and time out after 120s (editor tools can be slow).",
55961
+ "Start the editor before OR after zelari \u2014 unreachable servers are retried on each turn.",
55962
+ "Kill switch: disable the unreal-mcp server in mcp.json or ZELARI_MCP=0"
55963
+ ]
55964
+ };
55965
+ }
55637
55966
  function listMcpPresetIds() {
55638
55967
  return Object.keys(PRESETS).filter(
55639
- (k) => k === "cua" || k === "composio" || k === "qwen-mm-plugins"
55968
+ (k) => k === "cua" || k === "composio" || k === "qwen-mm-plugins" || k === "unreal-mcp"
55640
55969
  );
55641
55970
  }
55642
55971
  function getMcpPreset(id3) {
@@ -55705,7 +56034,10 @@ var init_mcpPresets = __esm({
55705
56034
  "cua-driver": () => CUA_DRIVER_PRESET,
55706
56035
  composio: buildComposioPreset,
55707
56036
  "qwen-mm-plugins": buildQwenMmPreset,
55708
- "qwen-mm": buildQwenMmPreset
56037
+ "qwen-mm": buildQwenMmPreset,
56038
+ unreal: buildUnrealPreset,
56039
+ "unreal-mcp": buildUnrealPreset,
56040
+ unrealEditor: buildUnrealPreset
55709
56041
  };
55710
56042
  }
55711
56043
  });
@@ -55735,7 +56067,9 @@ function readMcpConfig(projectRoot = process.cwd(), opts) {
55735
56067
  try {
55736
56068
  const parsed = JSON.parse(readFileSync30(p3, "utf8"));
55737
56069
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
55738
- if (!cfg || typeof cfg.command !== "string" || cfg.command.length === 0) continue;
56070
+ const hasCommand = !!cfg && typeof cfg.command === "string" && cfg.command.length > 0;
56071
+ const hasUrl = !!cfg && typeof cfg.url === "string" && /^https?:\/\//i.test(cfg.url);
56072
+ if (!cfg || !hasCommand && !hasUrl) continue;
55739
56073
  merged[name] = cfg;
55740
56074
  }
55741
56075
  } catch {
@@ -55776,9 +56110,44 @@ async function ensureLoaded(projectRoot) {
55776
56110
  }
55777
56111
  } catch (err) {
55778
56112
  client.close();
55779
- state2.warnings.push(`[mcp:${name}] disabled: ${err instanceof Error ? err.message : String(err)}`);
56113
+ const msg = err instanceof Error ? err.message : String(err);
56114
+ if (isHttpConfig(cfg)) {
56115
+ state2.pendingHttp.push({ name, cfg });
56116
+ state2.warnings.push(
56117
+ `[mcp:${name}] not reachable yet (${msg}) \u2014 will retry each turn`
56118
+ );
56119
+ } else {
56120
+ state2.warnings.push(`[mcp:${name}] disabled: ${msg}`);
56121
+ }
56122
+ }
56123
+ }
56124
+ }
56125
+ function isHttpConfig(cfg) {
56126
+ return cfg.type === "http" || !!cfg.url && !cfg.command;
56127
+ }
56128
+ async function retryPendingHttp() {
56129
+ if (state2.pendingHttp.length === 0) return;
56130
+ const remaining = [];
56131
+ for (const p3 of state2.pendingHttp) {
56132
+ const client = new McpClient(p3.name, p3.cfg);
56133
+ try {
56134
+ await client.start();
56135
+ const tools = await client.listTools();
56136
+ state2.clients.push(client);
56137
+ for (const info of tools) {
56138
+ state2.tools.push({
56139
+ registryName: sanitizeToolName(`mcp_${p3.name}_${info.name}`),
56140
+ serverName: p3.name,
56141
+ info,
56142
+ client
56143
+ });
56144
+ }
56145
+ } catch {
56146
+ client.close();
56147
+ remaining.push(p3);
55780
56148
  }
55781
56149
  }
56150
+ state2.pendingHttp = remaining;
55782
56151
  }
55783
56152
  function sanitizeToolName(raw) {
55784
56153
  return raw.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
@@ -55786,6 +56155,7 @@ function sanitizeToolName(raw) {
55786
56155
  async function registerMcpTools(registry4, projectRoot = process.cwd(), opts) {
55787
56156
  if (process.env["ZELARI_MCP"] === "0") return { registered: [], warnings: [] };
55788
56157
  await ensureLoaded(projectRoot);
56158
+ await retryPendingHttp();
55789
56159
  const skipCuaForCouncil = opts?.councilMode === true && !isCuaAllowedForCouncil();
55790
56160
  const registered = [];
55791
56161
  for (const t of state2.tools) {
@@ -55823,6 +56193,7 @@ function closeMcpClients() {
55823
56193
  state2.tools = [];
55824
56194
  state2.loaded = false;
55825
56195
  state2.warnings = [];
56196
+ state2.pendingHttp = [];
55826
56197
  }
55827
56198
  function _resetMcpForTests() {
55828
56199
  closeMcpClients();
@@ -55836,7 +56207,7 @@ var init_mcpManager = __esm({
55836
56207
  init_mcpClient();
55837
56208
  init_mcpPresets();
55838
56209
  init_folderTrust();
55839
- state2 = { loaded: false, tools: [], warnings: [], clients: [] };
56210
+ state2 = { loaded: false, tools: [], warnings: [], clients: [], pendingHttp: [] };
55840
56211
  }
55841
56212
  });
55842
56213
 
@@ -62016,7 +62387,7 @@ var MIN_NODE_MAJOR, STANDARD_BASH_PATHS2, POWERSHELL_EXES2, STANDARD_POWERSHELL_
62016
62387
  var init_prereqChecks = __esm({
62017
62388
  "src/cli/utils/prereqChecks.ts"() {
62018
62389
  "use strict";
62019
- MIN_NODE_MAJOR = 20;
62390
+ MIN_NODE_MAJOR = 24;
62020
62391
  STANDARD_BASH_PATHS2 = [
62021
62392
  "C:\\Program Files\\Git\\bin\\bash.exe",
62022
62393
  "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
@@ -67537,6 +67908,7 @@ var init_skillConfigIo = __esm({
67537
67908
  "@zelari/core/skills/builtin/planning",
67538
67909
  "@zelari/core/skills/builtin/refactoring",
67539
67910
  "@zelari/core/skills/builtin/review",
67911
+ "@zelari/core/skills/builtin/unrealEditor",
67540
67912
  "@zelari/core/skills/builtin/testing",
67541
67913
  "@zelari/core/skills/builtin/schema-loop",
67542
67914
  "@zelari/core/skills/builtin/computer-use-cua",
@@ -69851,12 +70223,15 @@ function checkNode(pkg) {
69851
70223
  return WARN(`could not parse node version: ${raw}`);
69852
70224
  }
69853
70225
  const major = Number(m[1]);
69854
- if (major < 20) {
70226
+ const enginesNode = pkg?.engines?.node;
70227
+ const requiredMajor = Number(enginesNode?.match(/\d+/)?.[0] ?? 20);
70228
+ if (major < requiredMajor) {
69855
70229
  return FAIL(
69856
- `node ${raw} is older than the required engines.node (>= 20.0.0)`
70230
+ `node ${raw} is older than the required engines.node (${enginesNode ?? ">= 20.0.0"})
70231
+ fix: install Node.js ${requiredMajor}+ (LTS) and reopen the terminal`
69857
70232
  );
69858
70233
  }
69859
- return OK(`node ${raw}`);
70234
+ return OK(`node ${raw} (engines.node ${enginesNode ?? ">= 20.0.0"})`);
69860
70235
  }
69861
70236
  function checkBundle() {
69862
70237
  const bundle = path88.join(packageRoot, "dist", "cli", "main.bundled.js");
@@ -80423,7 +80798,7 @@ function pickRootComponent() {
80423
80798
  }
80424
80799
  if (argv.includes("--help") || argv.includes("-h")) {
80425
80800
  console.log(
80426
- "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --inspect [--json] Unified project inspection (config, skills, MCP,\n hooks, plugins, AGENTS.md, trust status)\n --trust [path] Trust the cwd (or path) so project MCP + hooks load\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --fix-budget Set recommended ZELARI_MAX_TOOL_LOOP_HARD=180,\n ZELARI_MAX_TOOL_LOOP_ITERATIONS=60, ZELARI_CONTEXT_LIMIT=400000\n at User scope (prevents the agent stopping mid-task)\n --permission-mcp <socket> MCP stdio server for external agent permission prompts\n (spawned by claude --permission-prompt-tool)\n --memory-mcp Optional MCP stdio server for project memory\n --cwd <path> Trusted project root (requires ZELARI_MEMORY_MCP=1)\n --client-id <id> Stable local owner identity for private memories\n --memory-json <json> Read-only project-memory bridge for Zelari Desktop\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode kraken|council|zelari Dispatch mode (default: kraken; agent=alias)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n --profile <id> Capability profile (minimal/v1|kraken/v1|council/v1|mission/v1)\n --resume <id> Continue a 2.0 spine session\n --export-session <path> Write zelari-session-export/1 after the run\n --strict-done Evidence-based BUILD completion gate\n --session-export <id> Print a portable 2.0 session export (no LLM)\n serve Companion host for Android/remote clients (Tailscale)\n --bind <ip> Listen address (default: 127.0.0.1; use Tailscale IP)\n --port <n> Port (default: 7421)\n --token <secret> Bearer token (default: ~/.zelari-code/companion.token)\n --project <path> Allowlisted project root (repeatable)\n --save-projects Persist --project list to companion.json\n --print-config Print provider/model config as JSON (no secrets)\n --plugins-status JSON status of optional plugins (Playwright, eslint, \u2026)\n --plugins-install <id> Install plugin (playwright also fetches Chromium)\n --cwd <path> Workspace for -D installs (default: process.cwd())\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --thinking <spec> Thinking effort (auto|off|low|medium|high|budget:N)\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --login-oauth Start subscription OAuth (grok, chatgpt, anthropic)\n --provider <id> grok | chatgpt | anthropic\n --code <paste> Anthropic magic-link code (CODE#STATE)\n --no-browser Do not open the system browser\n --refresh-oauth Force-refresh an OAuth access token\n --provider <id> grok | chatgpt | anthropic\n --logout-oauth Clear stored OAuth credentials\n --provider <id> grok | chatgpt | anthropic\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable (required)\n --args <json> JSON array of args (optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-skills Print skills (builtin + user + project)\n --cwd <path> Project root for .zelari/skills\n --set-skill Add/update a SKILL.md skill\n --name <id> Skill id (required)\n --description <t> One-line description (required)\n --body <text> Markdown instructions (required)\n --category <c> plan|refactor|debug|review|test|docs|ops|git|db|maint\n --tools <csv> Comma-separated required tools (optional)\n --cost <l> low|medium|high (default: medium)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-skill Remove a user/project SKILL.md\n --name <id> --scope user|project [--cwd <path>]\n --generate-skill-from-url Fetch URL + draft skill via model (JSON)\n --url <https://...> Required\n --provider <id> Override active provider\n --model <name> Override model for the selected provider\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ZELARI_MEMORY_MCP=1 Enable the optional external memory MCP server\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
80801
+ "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --inspect [--json] Unified project inspection (config, skills, MCP,\n hooks, plugins, AGENTS.md, trust status)\n --trust [path] Trust the cwd (or path) so project MCP + hooks load\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --fix-budget Set recommended ZELARI_MAX_TOOL_LOOP_HARD=180,\n ZELARI_MAX_TOOL_LOOP_ITERATIONS=60, ZELARI_CONTEXT_LIMIT=400000\n at User scope (prevents the agent stopping mid-task)\n --permission-mcp <socket> MCP stdio server for external agent permission prompts\n (spawned by claude --permission-prompt-tool)\n --memory-mcp Optional MCP stdio server for project memory\n --cwd <path> Trusted project root (requires ZELARI_MEMORY_MCP=1)\n --client-id <id> Stable local owner identity for private memories\n --memory-json <json> Read-only project-memory bridge for Zelari Desktop\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode kraken|council|zelari Dispatch mode (default: kraken; agent=alias)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n --profile <id> Capability profile (minimal/v1|kraken/v1|council/v1|mission/v1)\n --resume <id> Continue a 2.0 spine session\n --export-session <path> Write zelari-session-export/1 after the run\n --strict-done Evidence-based BUILD completion gate\n --session-export <id> Print a portable 2.0 session export (no LLM)\n serve Companion host for Android/remote clients (Tailscale)\n --bind <ip> Listen address (default: 127.0.0.1; use Tailscale IP)\n --port <n> Port (default: 7421)\n --token <secret> Bearer token (default: ~/.zelari-code/companion.token)\n --project <path> Allowlisted project root (repeatable)\n --save-projects Persist --project list to companion.json\n --print-config Print provider/model config as JSON (no secrets)\n --plugins-status JSON status of optional plugins (Playwright, eslint, \u2026)\n --plugins-install <id> Install plugin (playwright also fetches Chromium)\n --cwd <path> Workspace for -D installs (default: process.cwd())\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --thinking <spec> Thinking effort (auto|off|low|medium|high|budget:N)\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --login-oauth Start subscription OAuth (grok, chatgpt, anthropic)\n --provider <id> grok | chatgpt | anthropic\n --code <paste> Anthropic magic-link code (CODE#STATE)\n --no-browser Do not open the system browser\n --refresh-oauth Force-refresh an OAuth access token\n --provider <id> grok | chatgpt | anthropic\n --logout-oauth Clear stored OAuth credentials\n --provider <id> grok | chatgpt | anthropic\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable for stdio servers\n --url <endpoint> Streamable HTTP endpoint (e.g. UE 5.8 editor)\n --args <json> JSON array of args (stdio, optional)\n --timeout <ms> Per-server request timeout (http, optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --preset unreal-mcp Unreal Engine 5.8+ editor (MCP over HTTP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-skills Print skills (builtin + user + project)\n --cwd <path> Project root for .zelari/skills\n --set-skill Add/update a SKILL.md skill\n --name <id> Skill id (required)\n --description <t> One-line description (required)\n --body <text> Markdown instructions (required)\n --category <c> plan|refactor|debug|review|test|docs|ops|git|db|maint\n --tools <csv> Comma-separated required tools (optional)\n --cost <l> low|medium|high (default: medium)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-skill Remove a user/project SKILL.md\n --name <id> --scope user|project [--cwd <path>]\n --generate-skill-from-url Fetch URL + draft skill via model (JSON)\n --url <https://...> Required\n --provider <id> Override active provider\n --model <name> Override model for the selected provider\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ZELARI_MEMORY_MCP=1 Enable the optional external memory MCP server\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
80427
80802
  );
80428
80803
  process.exit(0);
80429
80804
  }
@@ -80449,6 +80824,8 @@ function pickRootComponent() {
80449
80824
  };
80450
80825
  const name = get("--name");
80451
80826
  const command = get("--command");
80827
+ const url2 = get("--url");
80828
+ const timeoutRaw = get("--timeout");
80452
80829
  const scopeRaw = get("--scope") ?? "user";
80453
80830
  const scope = scopeRaw === "project" ? "project" : "user";
80454
80831
  const cwd = get("--cwd") ?? process.cwd();
@@ -80461,14 +80838,22 @@ function pickRootComponent() {
80461
80838
  if (!Array.isArray(parsed)) throw new Error("--args must be a JSON array");
80462
80839
  args = parsed.map(String);
80463
80840
  }
80464
- if (!name || !command) {
80465
- throw new Error("--name and --command are required");
80841
+ const timeoutMs2 = timeoutRaw !== void 0 ? Number(timeoutRaw) : void 0;
80842
+ if (timeoutMs2 !== void 0 && (!Number.isFinite(timeoutMs2) || timeoutMs2 <= 0)) {
80843
+ throw new Error("--timeout must be a positive number of milliseconds");
80844
+ }
80845
+ if (!name) throw new Error("--name is required");
80846
+ if (!command && !url2) {
80847
+ throw new Error("either --command (stdio) or --url (http) is required");
80848
+ }
80849
+ if (url2 && !/^https?:\/\//i.test(url2)) {
80850
+ throw new Error("--url must be an http(s) endpoint");
80466
80851
  }
80467
80852
  const result = upsertMcpServer({
80468
80853
  scope,
80469
80854
  name,
80470
80855
  projectRoot: cwd,
80471
- config: { command, args, enabled }
80856
+ config: url2 ? { type: "http", url: url2, timeoutMs: timeoutMs2, serial: true, enabled } : { command, args, enabled }
80472
80857
  });
80473
80858
  if (!result.ok) throw new Error(result.error);
80474
80859
  console.log(JSON.stringify({ ok: true, path: result.path, name, scope }));