reasonix 0.11.2 → 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";
@@ -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
  }
@@ -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>",
@@ -13130,6 +13317,150 @@ var handlers9 = {
13130
13317
  compact
13131
13318
  };
13132
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
+
13133
13464
  // src/cli/ui/slash/handlers/plans.ts
13134
13465
  import { basename } from "path";
13135
13466
  var plans = (_args, loop2) => {
@@ -13209,7 +13540,7 @@ var replay = (args, loop2) => {
13209
13540
  }
13210
13541
  };
13211
13542
  };
13212
- var handlers10 = {
13543
+ var handlers11 = {
13213
13544
  plans,
13214
13545
  replay
13215
13546
  };
@@ -13582,7 +13913,7 @@ async function readIndexMeta(rootDir) {
13582
13913
  return null;
13583
13914
  }
13584
13915
  }
13585
- var handlers11 = {
13916
+ var handlers12 = {
13586
13917
  semantic
13587
13918
  };
13588
13919
 
@@ -13617,7 +13948,7 @@ var forget = (_args, loop2) => {
13617
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?)`
13618
13949
  };
13619
13950
  };
13620
- var handlers12 = {
13951
+ var handlers13 = {
13621
13952
  sessions,
13622
13953
  forget
13623
13954
  };
@@ -13693,7 +14024,7 @@ ${found.body}${argsLine}`;
13693
14024
  resubmit: payload
13694
14025
  };
13695
14026
  };
13696
- var handlers13 = {
14027
+ var handlers14 = {
13697
14028
  skill,
13698
14029
  skills: skill
13699
14030
  };
@@ -13712,7 +14043,8 @@ var HANDLERS = {
13712
14043
  ...handlers10,
13713
14044
  ...handlers11,
13714
14045
  ...handlers12,
13715
- ...handlers13
14046
+ ...handlers13,
14047
+ ...handlers14
13716
14048
  };
13717
14049
  function handleSlash(cmd, args, loop2, ctx = {}) {
13718
14050
  const h = HANDLERS[cmd];
@@ -16513,7 +16845,7 @@ async function chatCommand(opts) {
16513
16845
  try {
16514
16846
  const spec = parseMcpSpec(raw);
16515
16847
  const prefix = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
16516
- 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 });
16517
16849
  const mcp3 = new McpClient({ transport });
16518
16850
  await mcp3.initialize();
16519
16851
  const bridge = await bridgeMcpTools(mcp3, {
@@ -16535,7 +16867,7 @@ async function chatCommand(opts) {
16535
16867
  };
16536
16868
  }
16537
16869
  const label = spec.name ?? "anon";
16538
- 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(" ")}`;
16539
16871
  process.stderr.write(
16540
16872
  `\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
16541
16873
  `
@@ -17664,7 +17996,7 @@ function makeTtyWriter() {
17664
17996
  // src/cli/commands/mcp-inspect.ts
17665
17997
  async function mcpInspectCommand(opts) {
17666
17998
  const spec = parseMcpSpec(opts.spec);
17667
- 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 });
17668
18000
  const client = new McpClient({ transport });
17669
18001
  try {
17670
18002
  await client.initialize();
@@ -17996,11 +18328,11 @@ async function runCommand2(opts) {
17996
18328
  try {
17997
18329
  const spec = parseMcpSpec(raw);
17998
18330
  const prefix2 = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
17999
- 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 });
18000
18332
  const mcp3 = new McpClient({ transport });
18001
18333
  await mcp3.initialize();
18002
18334
  const bridge = await bridgeMcpTools(mcp3, { registry: tools, namePrefix: prefix2 });
18003
- 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(" ")}`;
18004
18336
  process.stderr.write(
18005
18337
  `\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
18006
18338
  `