reasonix 0.11.1 → 0.11.3

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/dist/cli/index.js CHANGED
@@ -70,6 +70,29 @@ function addProjectShellAllowed(rootDir, prefix, path5 = defaultConfigPath()) {
70
70
  cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
71
71
  writeConfig(cfg, path5);
72
72
  }
73
+ function removeProjectShellAllowed(rootDir, prefix, path5 = defaultConfigPath()) {
74
+ const trimmed = prefix.trim();
75
+ if (!trimmed) return false;
76
+ const cfg = readConfig(path5);
77
+ const existing = cfg.projects?.[rootDir]?.shellAllowed ?? [];
78
+ if (!existing.includes(trimmed)) return false;
79
+ const next = existing.filter((p) => p !== trimmed);
80
+ if (!cfg.projects) cfg.projects = {};
81
+ if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
82
+ cfg.projects[rootDir].shellAllowed = next;
83
+ writeConfig(cfg, path5);
84
+ return true;
85
+ }
86
+ function clearProjectShellAllowed(rootDir, path5 = defaultConfigPath()) {
87
+ const cfg = readConfig(path5);
88
+ const existing = cfg.projects?.[rootDir]?.shellAllowed ?? [];
89
+ if (existing.length === 0) return 0;
90
+ if (!cfg.projects) cfg.projects = {};
91
+ if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
92
+ cfg.projects[rootDir].shellAllowed = [];
93
+ writeConfig(cfg, path5);
94
+ return existing.length;
95
+ }
73
96
  function loadEditMode(path5 = defaultConfigPath()) {
74
97
  const v = readConfig(path5).editMode;
75
98
  return v === "auto" ? "auto" : "review";
@@ -112,7 +135,7 @@ import { createParser } from "eventsource-parser";
112
135
 
113
136
  // src/retry.ts
114
137
  var DEFAULT_RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504];
115
- async function fetchWithRetry(fetchFn, url, init, opts = {}) {
138
+ async function fetchWithRetry(fetchFn, url, init2, opts = {}) {
116
139
  const maxAttempts = opts.maxAttempts ?? 4;
117
140
  const initial = opts.initialBackoffMs ?? 500;
118
141
  const cap = opts.maxBackoffMs ?? 1e4;
@@ -121,7 +144,7 @@ async function fetchWithRetry(fetchFn, url, init, opts = {}) {
121
144
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
122
145
  if (opts.signal?.aborted) throw new Error("aborted");
123
146
  try {
124
- const resp = await fetchFn(url, init);
147
+ const resp = await fetchFn(url, init2);
125
148
  if (resp.ok || !retryable.has(resp.status)) return resp;
126
149
  if (attempt === maxAttempts - 1) return resp;
127
150
  await resp.text().catch(() => void 0);
@@ -6760,6 +6783,159 @@ var SseTransport = class {
6760
6783
  }
6761
6784
  };
6762
6785
 
6786
+ // src/mcp/streamable-http.ts
6787
+ import { createParser as createParser3 } from "eventsource-parser";
6788
+ var SESSION_HEADER = "mcp-session-id";
6789
+ var StreamableHttpTransport = class {
6790
+ url;
6791
+ extraHeaders;
6792
+ queue = [];
6793
+ waiters = [];
6794
+ controller = new AbortController();
6795
+ /** Session id minted by server on (typically) the initialize response. */
6796
+ sessionId = null;
6797
+ closed = false;
6798
+ /** Background SSE read-loops kicked off by send(); awaited on close(). */
6799
+ streams = /* @__PURE__ */ new Set();
6800
+ constructor(opts) {
6801
+ this.url = opts.url;
6802
+ this.extraHeaders = opts.headers ?? {};
6803
+ }
6804
+ async send(message) {
6805
+ if (this.closed) throw new Error("MCP Streamable HTTP transport is closed");
6806
+ const headers = {
6807
+ "content-type": "application/json",
6808
+ // Both accepted — server picks. application/json first signals a
6809
+ // mild preference for the simpler shape when the response is a
6810
+ // single message.
6811
+ accept: "application/json, text/event-stream",
6812
+ ...this.extraHeaders
6813
+ };
6814
+ if (this.sessionId !== null) headers["mcp-session-id"] = this.sessionId;
6815
+ let res;
6816
+ try {
6817
+ res = await fetch(this.url, {
6818
+ method: "POST",
6819
+ headers,
6820
+ body: JSON.stringify(message),
6821
+ signal: this.controller.signal
6822
+ });
6823
+ } catch (err) {
6824
+ throw new Error(`MCP Streamable HTTP POST ${this.url} failed: ${err.message}`);
6825
+ }
6826
+ const serverSessionId = res.headers.get(SESSION_HEADER);
6827
+ if (serverSessionId && this.sessionId === null) {
6828
+ this.sessionId = serverSessionId;
6829
+ }
6830
+ if (res.status === 404 && this.sessionId !== null) {
6831
+ await res.body?.cancel().catch(() => void 0);
6832
+ throw new Error(
6833
+ `MCP Streamable HTTP session expired (server returned 404 with Mcp-Session-Id "${this.sessionId}"). Reinitialize the client.`
6834
+ );
6835
+ }
6836
+ if (!res.ok) {
6837
+ const body = await res.text().catch(() => "");
6838
+ throw new Error(
6839
+ `MCP Streamable HTTP POST ${this.url} \u2192 ${res.status} ${res.statusText}${body ? `: ${body}` : ""}`
6840
+ );
6841
+ }
6842
+ if (res.status === 202) {
6843
+ await res.body?.cancel().catch(() => void 0);
6844
+ return;
6845
+ }
6846
+ const ct = (res.headers.get("content-type") ?? "").toLowerCase();
6847
+ if (ct.includes("application/json")) {
6848
+ let parsed;
6849
+ try {
6850
+ parsed = await res.json();
6851
+ } catch (err) {
6852
+ throw new Error(`MCP Streamable HTTP body wasn't valid JSON: ${err.message}`);
6853
+ }
6854
+ if (Array.isArray(parsed)) {
6855
+ for (const item of parsed) this.pushMessage(item);
6856
+ } else {
6857
+ this.pushMessage(parsed);
6858
+ }
6859
+ return;
6860
+ }
6861
+ if (ct.includes("text/event-stream")) {
6862
+ if (!res.body) {
6863
+ throw new Error("MCP Streamable HTTP SSE response had no body");
6864
+ }
6865
+ const stream = this.consumeStream(res.body);
6866
+ this.streams.add(stream);
6867
+ stream.finally(() => this.streams.delete(stream));
6868
+ return;
6869
+ }
6870
+ await res.body?.cancel().catch(() => void 0);
6871
+ }
6872
+ async *messages() {
6873
+ while (true) {
6874
+ if (this.queue.length > 0) {
6875
+ yield this.queue.shift();
6876
+ continue;
6877
+ }
6878
+ if (this.closed) return;
6879
+ const next = await new Promise((resolve12) => {
6880
+ this.waiters.push(resolve12);
6881
+ });
6882
+ if (next === null) return;
6883
+ yield next;
6884
+ }
6885
+ }
6886
+ async close() {
6887
+ if (this.closed) return;
6888
+ this.closed = true;
6889
+ while (this.waiters.length > 0) this.waiters.shift()(null);
6890
+ try {
6891
+ this.controller.abort();
6892
+ } catch {
6893
+ }
6894
+ await Promise.allSettled(Array.from(this.streams));
6895
+ }
6896
+ /** Visible for tests — confirm session header round-trip. */
6897
+ getSessionId() {
6898
+ return this.sessionId;
6899
+ }
6900
+ // ---------- internals ----------
6901
+ async consumeStream(body) {
6902
+ const parser = createParser3({
6903
+ onEvent: (ev) => {
6904
+ const type = ev.event ?? "message";
6905
+ if (type !== "message") return;
6906
+ try {
6907
+ const parsed = JSON.parse(ev.data);
6908
+ this.pushMessage(parsed);
6909
+ } catch {
6910
+ }
6911
+ }
6912
+ });
6913
+ const decoder = new TextDecoder();
6914
+ try {
6915
+ for await (const chunk of body) {
6916
+ if (this.closed) break;
6917
+ parser.feed(decoder.decode(chunk, { stream: true }));
6918
+ }
6919
+ } catch (err) {
6920
+ if (!this.closed) {
6921
+ this.pushMessage({
6922
+ jsonrpc: "2.0",
6923
+ id: null,
6924
+ error: {
6925
+ code: -32e3,
6926
+ message: `Streamable HTTP stream error: ${err.message}`
6927
+ }
6928
+ });
6929
+ }
6930
+ }
6931
+ }
6932
+ pushMessage(msg) {
6933
+ const waiter = this.waiters.shift();
6934
+ if (waiter) waiter(msg);
6935
+ else this.queue.push(msg);
6936
+ }
6937
+ };
6938
+
6763
6939
  // src/mcp/shell-split.ts
6764
6940
  function shellSplit(input) {
6765
6941
  const tokens = [];
@@ -6812,6 +6988,7 @@ function shellSplit(input) {
6812
6988
  // src/mcp/spec.ts
6813
6989
  var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
6814
6990
  var HTTP_URL = /^https?:\/\//i;
6991
+ var STREAMABLE_PREFIX = /^streamable\+(https?:\/\/.+)$/i;
6815
6992
  function parseMcpSpec(input) {
6816
6993
  const trimmed = input.trim();
6817
6994
  if (!trimmed) {
@@ -6823,6 +7000,10 @@ function parseMcpSpec(input) {
6823
7000
  if (!body) {
6824
7001
  throw new Error(`MCP spec has name but no command: ${input}`);
6825
7002
  }
7003
+ const streamMatch = STREAMABLE_PREFIX.exec(body);
7004
+ if (streamMatch) {
7005
+ return { transport: "streamable-http", name, url: streamMatch[1] };
7006
+ }
6826
7007
  if (HTTP_URL.test(body)) {
6827
7008
  return { transport: "sse", name, url: body };
6828
7009
  }
@@ -7254,12 +7435,12 @@ function formatLogSize(path5 = defaultUsageLogPath()) {
7254
7435
  }
7255
7436
 
7256
7437
  // src/cli/commands/chat.tsx
7257
- import { existsSync as existsSync15, statSync as statSync9 } from "fs";
7438
+ import { existsSync as existsSync16, statSync as statSync9 } from "fs";
7258
7439
  import { render } from "ink";
7259
7440
  import React27, { useState as useState12 } from "react";
7260
7441
 
7261
7442
  // src/cli/ui/App.tsx
7262
- import * as pathMod6 from "path";
7443
+ import * as pathMod7 from "path";
7263
7444
  import { Box as Box22, Static, Text as Text20, useApp, useStdout as useStdout8 } from "ink";
7264
7445
  import React24, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
7265
7446
 
@@ -11579,6 +11760,12 @@ var SLASH_COMMANDS = [
11579
11760
  argsHint: "[reload]",
11580
11761
  summary: "list active hooks (settings.json under .reasonix/) \xB7 reload re-reads from disk"
11581
11762
  },
11763
+ {
11764
+ cmd: "permissions",
11765
+ argsHint: "[list|add <prefix>|remove <prefix|N>|clear confirm]",
11766
+ summary: "show / edit shell allowlist (builtin read-only \xB7 per-project: ~/.reasonix/config.json)",
11767
+ argCompleter: ["list", "add", "remove", "clear"]
11768
+ },
11582
11769
  {
11583
11770
  cmd: "cwd",
11584
11771
  argsHint: "<path>",
@@ -11626,6 +11813,13 @@ var SLASH_COMMANDS = [
11626
11813
  },
11627
11814
  { cmd: "exit", summary: "quit the TUI" },
11628
11815
  // Code-mode only
11816
+ {
11817
+ cmd: "init",
11818
+ argsHint: "[force]",
11819
+ summary: "scan the project and synthesize a baseline REASONIX.md (model writes; review with /apply). `force` overwrites an existing file.",
11820
+ contextual: "code",
11821
+ argCompleter: ["force"]
11822
+ },
11629
11823
  {
11630
11824
  cmd: "apply",
11631
11825
  argsHint: "[N|N,M|N-M]",
@@ -12460,6 +12654,103 @@ var handlers3 = {
12460
12654
  walk: walk2
12461
12655
  };
12462
12656
 
12657
+ // src/cli/ui/slash/handlers/init.ts
12658
+ import { existsSync as existsSync15 } from "fs";
12659
+ import * as pathMod6 from "path";
12660
+ var INIT_PROMPT = [
12661
+ "# Task: Initialize REASONIX.md",
12662
+ "",
12663
+ "I want you to generate a REASONIX.md at the project root that captures",
12664
+ "the working knowledge a future Reasonix session needs to be productive",
12665
+ "here. This file is auto-pinned into your system prompt every launch,",
12666
+ "so its size and accuracy matter.",
12667
+ "",
12668
+ "## Hard constraints (do NOT relax these)",
12669
+ "",
12670
+ "- **Length cap: \u2264 80 lines / 3KB total.** Be concise. If you can't fit a",
12671
+ " section, drop it.",
12672
+ "- **Only document things you can verify by reading files.** Do NOT",
12673
+ " speculate about architectural intent, future roadmap, or design",
12674
+ " rationale. If it isn't obvious from the code, leave it out.",
12675
+ "- **No placeholder text.** No 'TODO: describe X', no 'Add more here'.",
12676
+ " Either state a fact or omit the section.",
12677
+ "",
12678
+ "## Procedure",
12679
+ "",
12680
+ "1. Read the top of any existing README* file.",
12681
+ "2. Read the manifest (package.json / Cargo.toml / pyproject.toml /",
12682
+ " go.mod / etc.) \u2014 pick whichever exists.",
12683
+ "3. `directory_tree` 1-2 levels deep on the project root, skipping",
12684
+ " common build/dependency dirs (node_modules, dist, target, .git,",
12685
+ " venv, __pycache__).",
12686
+ "4. Identify: primary language + framework, top-level layout, test",
12687
+ " runner, lint/format setup, build/run/test scripts, any non-obvious",
12688
+ " convention with visible evidence (commit message format, import",
12689
+ " order, naming pattern).",
12690
+ "5. Write REASONIX.md with the sections below, skipping any you can't",
12691
+ " fill from evidence.",
12692
+ "",
12693
+ "## Sections to use (skip ones with no evidence)",
12694
+ "",
12695
+ "- **Stack** \u2014 language + framework + 3-5 key deps. One line each.",
12696
+ "- **Layout** \u2014 top-level dirs and what lives in each. One line each.",
12697
+ "- **Commands** \u2014 verbatim from `scripts` block (or equivalent):",
12698
+ " build / test / lint / typecheck / dev / format. Whatever exists.",
12699
+ "- **Conventions** \u2014 only things visible in the code. Examples:",
12700
+ " '*.test.ts colocated with source', 'named exports only',",
12701
+ " 'commits use Conventional Commits prefix'. If you can't find any",
12702
+ " CONVENTION evidence, omit the whole section.",
12703
+ "- **Watch out for** \u2014 gotchas a new contributor would benefit from",
12704
+ " knowing BEFORE editing. Examples: 'edit_file SEARCH must match",
12705
+ " byte-for-byte', 'this dir is generated, don't edit by hand'.",
12706
+ " Omit if you find nothing concrete.",
12707
+ "",
12708
+ "## Output",
12709
+ "",
12710
+ "Write the result to `REASONIX.md` in the project root using the",
12711
+ "filesystem tools (edit_file with empty SEARCH if creating new,",
12712
+ "write_file if overwriting). After writing, STOP \u2014 do not summarize",
12713
+ "what you did, do not propose follow-up tasks. The user will review",
12714
+ "the pending edit via /apply.",
12715
+ "",
12716
+ "Start now."
12717
+ ].join("\n");
12718
+ var init = (args, _loop, ctx) => {
12719
+ if (!ctx.codeRoot) {
12720
+ return {
12721
+ info: [
12722
+ "/init only works in code mode (it needs filesystem tools).",
12723
+ "Run `reasonix code [path]` to start a session rooted at the",
12724
+ "project you want to initialize, then run /init."
12725
+ ].join("\n")
12726
+ };
12727
+ }
12728
+ const force = (args[0] ?? "").toLowerCase() === "force";
12729
+ const target = pathMod6.join(ctx.codeRoot, "REASONIX.md");
12730
+ if (existsSync15(target) && !force) {
12731
+ return {
12732
+ info: [
12733
+ `\u25B8 REASONIX.md already exists at ${target}`,
12734
+ "",
12735
+ " /init force regenerate from scratch (overwrites)",
12736
+ "",
12737
+ " Or edit it by hand \u2014 it's just markdown. The current file is",
12738
+ " pinned into the system prompt every launch as-is."
12739
+ ].join("\n")
12740
+ };
12741
+ }
12742
+ return {
12743
+ info: [
12744
+ "\u25B8 /init \u2014 model will scan the project and synthesize REASONIX.md.",
12745
+ " The result lands as a pending edit; review with /apply or /walk."
12746
+ ].join("\n"),
12747
+ resubmit: INIT_PROMPT
12748
+ };
12749
+ };
12750
+ var handlers4 = {
12751
+ init
12752
+ };
12753
+
12463
12754
  // src/cli/ui/slash/handlers/jobs.ts
12464
12755
  var jobs = (_args, _loop, ctx) => {
12465
12756
  if (!ctx.jobs) {
@@ -12515,7 +12806,7 @@ $ ${out.command}`;
12515
12806
  return { info: out.output ? `${header2}
12516
12807
  ${out.output}` : header2 };
12517
12808
  };
12518
- var handlers4 = {
12809
+ var handlers5 = {
12519
12810
  jobs,
12520
12811
  kill,
12521
12812
  logs
@@ -12576,7 +12867,7 @@ var mcp = (_args, loop2, ctx) => {
12576
12867
  lines.push("To change this set, exit and run `reasonix setup`.");
12577
12868
  return { info: lines.join("\n") };
12578
12869
  };
12579
- var handlers5 = { mcp };
12870
+ var handlers6 = { mcp };
12580
12871
 
12581
12872
  // src/cli/ui/slash/handlers/memory.ts
12582
12873
  var memory = (args, _loop, ctx) => {
@@ -12711,7 +13002,7 @@ var memory = (args, _loop, ctx) => {
12711
13002
  );
12712
13003
  return { info: parts.join("\n") };
12713
13004
  };
12714
- var handlers6 = { memory };
13005
+ var handlers7 = { memory };
12715
13006
 
12716
13007
  // src/cli/ui/slash/handlers/model.ts
12717
13008
  var model = (args, loop2, ctx) => {
@@ -12864,7 +13155,7 @@ var pro = (args, loop2, ctx) => {
12864
13155
  };
12865
13156
  };
12866
13157
  var ESCALATION_MODEL_ID = "deepseek-v4-pro";
12867
- var handlers7 = {
13158
+ var handlers8 = {
12868
13159
  model,
12869
13160
  models,
12870
13161
  harvest: harvest2,
@@ -13017,7 +13308,7 @@ var compact = (args, loop2) => {
13017
13308
  info: `\u25B8 compacted ${healedCount} payload(s) to ${cap.toLocaleString()} tokens each (tool results + tool-call args), saved ${tokensSaved.toLocaleString()} tokens (${charsSaved.toLocaleString()} chars). Session file rewritten.`
13018
13309
  };
13019
13310
  };
13020
- var handlers8 = {
13311
+ var handlers9 = {
13021
13312
  think,
13022
13313
  reasoning: think,
13023
13314
  tool,
@@ -13026,6 +13317,150 @@ var handlers8 = {
13026
13317
  compact
13027
13318
  };
13028
13319
 
13320
+ // src/cli/ui/slash/handlers/permissions.ts
13321
+ var permissions = (args, _loop, ctx) => {
13322
+ const sub = (args[0] ?? "").toLowerCase();
13323
+ const root = ctx.codeRoot;
13324
+ const mode2 = ctx.editMode ?? null;
13325
+ if (sub === "" || sub === "list" || sub === "ls") {
13326
+ return { info: renderListing(root, mode2) };
13327
+ }
13328
+ if (!root) {
13329
+ return {
13330
+ info: "/permissions add / remove / clear are only available inside `reasonix code` \u2014 they edit the project-scoped allowlist (`~/.reasonix/config.json` projects[<root>].shellAllowed)."
13331
+ };
13332
+ }
13333
+ if (sub === "add") {
13334
+ const prefix = args.slice(1).join(" ").trim();
13335
+ if (!prefix) {
13336
+ return {
13337
+ info: 'usage: /permissions add <prefix> (multi-token OK: /permissions add "git push origin")'
13338
+ };
13339
+ }
13340
+ const before = loadProjectShellAllowed(root);
13341
+ if (before.includes(prefix)) {
13342
+ return { info: `\u25B8 already allowed: ${prefix}` };
13343
+ }
13344
+ if (BUILTIN_ALLOWLIST.includes(prefix)) {
13345
+ return {
13346
+ info: `\u25B8 \`${prefix}\` is already in the builtin allowlist \u2014 no per-project entry needed. (Builtin entries are always on.)`
13347
+ };
13348
+ }
13349
+ addProjectShellAllowed(root, prefix);
13350
+ return {
13351
+ info: `\u25B8 added: ${prefix}
13352
+ \u2192 next \`${prefix}\` invocation runs without prompting in this project.`
13353
+ };
13354
+ }
13355
+ if (sub === "remove" || sub === "rm" || sub === "delete") {
13356
+ const target = args.slice(1).join(" ").trim();
13357
+ if (!target) {
13358
+ return {
13359
+ info: "usage: /permissions remove <prefix-or-index> (e.g. /permissions remove 3, or /permissions remove npm)"
13360
+ };
13361
+ }
13362
+ const existing = loadProjectShellAllowed(root);
13363
+ let prefix = null;
13364
+ if (/^\d+$/.test(target)) {
13365
+ const idx = Number.parseInt(target, 10);
13366
+ if (idx < 1 || idx > existing.length) {
13367
+ return {
13368
+ info: existing.length === 0 ? "\u25B8 no project allowlist entries to remove." : `\u25B8 index out of range: ${idx} (project list has ${existing.length} entries)`
13369
+ };
13370
+ }
13371
+ prefix = existing[idx - 1] ?? null;
13372
+ } else {
13373
+ prefix = target;
13374
+ }
13375
+ if (prefix === null) return { info: "\u25B8 nothing to remove." };
13376
+ if (BUILTIN_ALLOWLIST.includes(prefix) && !existing.includes(prefix)) {
13377
+ return {
13378
+ info: `\u25B8 \`${prefix}\` is in the builtin allowlist (read-only). Builtin entries can't be removed at runtime \u2014 they're baked into the binary.`
13379
+ };
13380
+ }
13381
+ const ok = removeProjectShellAllowed(root, prefix);
13382
+ return {
13383
+ info: ok ? `\u25B8 removed: ${prefix}` : `\u25B8 no such project entry: ${prefix} (try /permissions list to see what's stored)`
13384
+ };
13385
+ }
13386
+ if (sub === "clear") {
13387
+ if ((args[1] ?? "").toLowerCase() !== "confirm") {
13388
+ const count = loadProjectShellAllowed(root).length;
13389
+ return {
13390
+ info: count === 0 ? "\u25B8 project allowlist is already empty." : `about to drop ${count} project allowlist entr${count === 1 ? "y" : "ies"} for ${root}. Re-run with the word 'confirm' to proceed: /permissions clear confirm`
13391
+ };
13392
+ }
13393
+ const dropped = clearProjectShellAllowed(root);
13394
+ return {
13395
+ info: dropped === 0 ? "\u25B8 project allowlist was already empty \u2014 nothing changed." : `\u25B8 cleared ${dropped} project allowlist entr${dropped === 1 ? "y" : "ies"}.`
13396
+ };
13397
+ }
13398
+ return {
13399
+ info: [
13400
+ "usage: /permissions [list] show current state",
13401
+ ' /permissions add <prefix> persist (e.g. "npm run build")',
13402
+ " /permissions remove <prefix-or-N> drop one entry",
13403
+ " /permissions clear confirm wipe every project entry"
13404
+ ].join("\n")
13405
+ };
13406
+ };
13407
+ function renderListing(root, mode2) {
13408
+ const lines = [];
13409
+ if (mode2 === "yolo") {
13410
+ lines.push(
13411
+ "\u25B8 edit mode: YOLO \u2014 every shell command auto-runs, allowlist is bypassed. /mode review to re-enable prompts."
13412
+ );
13413
+ } else if (mode2 === "auto") {
13414
+ lines.push(
13415
+ "\u25B8 edit mode: auto \u2014 edits auto-apply, shell still gated by allowlist (or ShellConfirm prompt for non-allowlisted)."
13416
+ );
13417
+ } else if (mode2 === "review") {
13418
+ lines.push(
13419
+ "\u25B8 edit mode: review \u2014 both edits and non-allowlisted shell commands ask before running."
13420
+ );
13421
+ }
13422
+ lines.push("");
13423
+ if (root) {
13424
+ const project = loadProjectShellAllowed(root);
13425
+ lines.push(`Project allowlist (${project.length}) \u2014 ${root}`);
13426
+ if (project.length === 0) {
13427
+ lines.push(' (none \u2014 pick "always allow" on a ShellConfirm prompt to add one,');
13428
+ lines.push(" or `/permissions add <prefix>` directly.)");
13429
+ } else {
13430
+ project.forEach((p, i) => {
13431
+ lines.push(` ${String(i + 1).padStart(2)}. ${p}`);
13432
+ });
13433
+ }
13434
+ } else {
13435
+ lines.push("Project allowlist \u2014 (no project root; chat mode shows builtin entries only)");
13436
+ }
13437
+ lines.push("");
13438
+ lines.push(`Builtin allowlist (${BUILTIN_ALLOWLIST.length}) \u2014 read-only, baked in`);
13439
+ const grouped = /* @__PURE__ */ new Map();
13440
+ for (const entry of BUILTIN_ALLOWLIST) {
13441
+ const head = entry.split(" ")[0] ?? entry;
13442
+ if (!grouped.has(head)) grouped.set(head, []);
13443
+ grouped.get(head).push(entry);
13444
+ }
13445
+ for (const [head, items] of grouped) {
13446
+ if (items.length === 1 && items[0] === head) {
13447
+ lines.push(` \xB7 ${head}`);
13448
+ } else {
13449
+ const tail = items.map((i) => i.slice(head.length).trim() || "(bare)").join(", ");
13450
+ lines.push(` \xB7 ${head}: ${tail}`);
13451
+ }
13452
+ }
13453
+ lines.push("");
13454
+ lines.push(
13455
+ "Subcommands: /permissions add <prefix> \xB7 /permissions remove <prefix-or-N> \xB7 /permissions clear confirm"
13456
+ );
13457
+ return lines.join("\n");
13458
+ }
13459
+ var handlers10 = {
13460
+ permissions,
13461
+ perms: permissions
13462
+ };
13463
+
13029
13464
  // src/cli/ui/slash/handlers/plans.ts
13030
13465
  import { basename } from "path";
13031
13466
  var plans = (_args, loop2) => {
@@ -13105,7 +13540,7 @@ var replay = (args, loop2) => {
13105
13540
  }
13106
13541
  };
13107
13542
  };
13108
- var handlers9 = {
13543
+ var handlers11 = {
13109
13544
  plans,
13110
13545
  replay
13111
13546
  };
@@ -13478,7 +13913,7 @@ async function readIndexMeta(rootDir) {
13478
13913
  return null;
13479
13914
  }
13480
13915
  }
13481
- var handlers10 = {
13916
+ var handlers12 = {
13482
13917
  semantic
13483
13918
  };
13484
13919
 
@@ -13513,7 +13948,7 @@ var forget = (_args, loop2) => {
13513
13948
  info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
13514
13949
  };
13515
13950
  };
13516
- var handlers11 = {
13951
+ var handlers13 = {
13517
13952
  sessions,
13518
13953
  forget
13519
13954
  };
@@ -13589,7 +14024,7 @@ ${found.body}${argsLine}`;
13589
14024
  resubmit: payload
13590
14025
  };
13591
14026
  };
13592
- var handlers12 = {
14027
+ var handlers14 = {
13593
14028
  skill,
13594
14029
  skills: skill
13595
14030
  };
@@ -13607,7 +14042,9 @@ var HANDLERS = {
13607
14042
  ...handlers9,
13608
14043
  ...handlers10,
13609
14044
  ...handlers11,
13610
- ...handlers12
14045
+ ...handlers12,
14046
+ ...handlers13,
14047
+ ...handlers14
13611
14048
  };
13612
14049
  function handleSlash(cmd, args, loop2, ctx = {}) {
13613
14050
  const h = HANDLERS[cmd];
@@ -15403,8 +15840,8 @@ function App({
15403
15840
  const parsed = JSON.parse(ev.toolArgs);
15404
15841
  if (typeof parsed.path === "string" && parsed.path.trim()) {
15405
15842
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
15406
- const expanded = parsed.path.startsWith("~") && home ? pathMod6.join(home, parsed.path.slice(1)) : parsed.path;
15407
- const abs = pathMod6.resolve(expanded);
15843
+ const expanded = parsed.path.startsWith("~") && home ? pathMod7.join(home, parsed.path.slice(1)) : parsed.path;
15844
+ const abs = pathMod7.resolve(expanded);
15408
15845
  setPendingWorkspace({ path: abs });
15409
15846
  }
15410
15847
  } catch {
@@ -16408,7 +16845,7 @@ async function chatCommand(opts) {
16408
16845
  try {
16409
16846
  const spec = parseMcpSpec(raw);
16410
16847
  const prefix = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
16411
- const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
16848
+ const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : spec.transport === "streamable-http" ? new StreamableHttpTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
16412
16849
  const mcp3 = new McpClient({ transport });
16413
16850
  await mcp3.initialize();
16414
16851
  const bridge = await bridgeMcpTools(mcp3, {
@@ -16430,7 +16867,7 @@ async function chatCommand(opts) {
16430
16867
  };
16431
16868
  }
16432
16869
  const label = spec.name ?? "anon";
16433
- const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
16870
+ const source = spec.transport === "sse" || spec.transport === "streamable-http" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
16434
16871
  process.stderr.write(
16435
16872
  `\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
16436
16873
  `
@@ -16473,7 +16910,7 @@ async function chatCommand(opts) {
16473
16910
  const prior = loadSessionMessages(opts.session);
16474
16911
  if (prior.length > 0) {
16475
16912
  const p = sessionPath(opts.session);
16476
- const mtime = existsSync15(p) ? statSync9(p).mtime : /* @__PURE__ */ new Date();
16913
+ const mtime = existsSync16(p) ? statSync9(p).mtime : /* @__PURE__ */ new Date();
16477
16914
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
16478
16915
  }
16479
16916
  } else if (opts.session && opts.forceNew) {
@@ -17559,7 +17996,7 @@ function makeTtyWriter() {
17559
17996
  // src/cli/commands/mcp-inspect.ts
17560
17997
  async function mcpInspectCommand(opts) {
17561
17998
  const spec = parseMcpSpec(opts.spec);
17562
- const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
17999
+ const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : spec.transport === "streamable-http" ? new StreamableHttpTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
17563
18000
  const client = new McpClient({ transport });
17564
18001
  try {
17565
18002
  await client.initialize();
@@ -17891,11 +18328,11 @@ async function runCommand2(opts) {
17891
18328
  try {
17892
18329
  const spec = parseMcpSpec(raw);
17893
18330
  const prefix2 = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
17894
- const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
18331
+ const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : spec.transport === "streamable-http" ? new StreamableHttpTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
17895
18332
  const mcp3 = new McpClient({ transport });
17896
18333
  await mcp3.initialize();
17897
18334
  const bridge = await bridgeMcpTools(mcp3, { registry: tools, namePrefix: prefix2 });
17898
- const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
18335
+ const source = spec.transport === "sse" || spec.transport === "streamable-http" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
17899
18336
  process.stderr.write(
17900
18337
  `\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
17901
18338
  `