reasonix 0.3.0-alpha.3 → 0.3.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli/index.js +205 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +96 -2
- package/dist/index.js +216 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,6 +131,11 @@ reasonix chat \
|
|
|
131
131
|
--mcp "fs=npx -y @modelcontextprotocol/server-filesystem /tmp/safe" \
|
|
132
132
|
--mcp "mem=npx -y @modelcontextprotocol/server-memory"
|
|
133
133
|
# Tools land in a shared registry as fs_read_file, mem_set, etc.
|
|
134
|
+
|
|
135
|
+
# Remote / hosted MCP server — pass an http(s) URL instead of a command.
|
|
136
|
+
# Reasonix opens an SSE stream and POSTs JSON-RPC to the endpoint the
|
|
137
|
+
# server advertises (MCP 2024-11-05 HTTP+SSE transport).
|
|
138
|
+
reasonix chat --mcp "kb=https://mcp.example.com/sse"
|
|
134
139
|
```
|
|
135
140
|
|
|
136
141
|
[mcp]: ./benchmarks/tau-bench/transcripts/mcp-demo.add.jsonl
|
package/dist/cli/index.js
CHANGED
|
@@ -2103,6 +2103,145 @@ function quoteArg(s, windows) {
|
|
|
2103
2103
|
return `"${s.replace(/"/g, '""')}"`;
|
|
2104
2104
|
}
|
|
2105
2105
|
|
|
2106
|
+
// src/mcp/sse.ts
|
|
2107
|
+
import { createParser as createParser2 } from "eventsource-parser";
|
|
2108
|
+
var SseTransport = class {
|
|
2109
|
+
url;
|
|
2110
|
+
headers;
|
|
2111
|
+
queue = [];
|
|
2112
|
+
waiters = [];
|
|
2113
|
+
controller = new AbortController();
|
|
2114
|
+
closed = false;
|
|
2115
|
+
postUrl = null;
|
|
2116
|
+
endpointReady;
|
|
2117
|
+
resolveEndpoint;
|
|
2118
|
+
rejectEndpoint;
|
|
2119
|
+
constructor(opts) {
|
|
2120
|
+
this.url = opts.url;
|
|
2121
|
+
this.headers = opts.headers ?? {};
|
|
2122
|
+
this.endpointReady = new Promise((resolve2, reject) => {
|
|
2123
|
+
this.resolveEndpoint = resolve2;
|
|
2124
|
+
this.rejectEndpoint = reject;
|
|
2125
|
+
});
|
|
2126
|
+
this.endpointReady.catch(() => void 0);
|
|
2127
|
+
void this.runStream();
|
|
2128
|
+
}
|
|
2129
|
+
async send(message) {
|
|
2130
|
+
if (this.closed) throw new Error("MCP SSE transport is closed");
|
|
2131
|
+
const postUrl = await this.endpointReady;
|
|
2132
|
+
const res = await fetch(postUrl, {
|
|
2133
|
+
method: "POST",
|
|
2134
|
+
headers: { "content-type": "application/json", ...this.headers },
|
|
2135
|
+
body: JSON.stringify(message),
|
|
2136
|
+
signal: this.controller.signal
|
|
2137
|
+
});
|
|
2138
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
2139
|
+
if (!res.ok) {
|
|
2140
|
+
throw new Error(`MCP SSE POST ${postUrl} failed: ${res.status} ${res.statusText}`);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
async *messages() {
|
|
2144
|
+
while (true) {
|
|
2145
|
+
if (this.queue.length > 0) {
|
|
2146
|
+
yield this.queue.shift();
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
if (this.closed) return;
|
|
2150
|
+
const next = await new Promise((resolve2) => {
|
|
2151
|
+
this.waiters.push(resolve2);
|
|
2152
|
+
});
|
|
2153
|
+
if (next === null) return;
|
|
2154
|
+
yield next;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
async close() {
|
|
2158
|
+
if (this.closed) return;
|
|
2159
|
+
this.closed = true;
|
|
2160
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2161
|
+
this.rejectEndpoint(new Error("MCP SSE transport closed before endpoint was ready"));
|
|
2162
|
+
try {
|
|
2163
|
+
this.controller.abort();
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// ---------- internals ----------
|
|
2168
|
+
async runStream() {
|
|
2169
|
+
let res;
|
|
2170
|
+
try {
|
|
2171
|
+
res = await fetch(this.url, {
|
|
2172
|
+
method: "GET",
|
|
2173
|
+
headers: { accept: "text/event-stream", ...this.headers },
|
|
2174
|
+
signal: this.controller.signal
|
|
2175
|
+
});
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
this.failHandshake(`SSE connect to ${this.url} failed: ${err.message}`);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (!res.ok || !res.body) {
|
|
2181
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2182
|
+
this.failHandshake(`SSE handshake ${this.url} \u2192 ${res.status} ${res.statusText}`);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const parser = createParser2({
|
|
2186
|
+
onEvent: (ev) => this.handleEvent(ev.event ?? "message", ev.data)
|
|
2187
|
+
});
|
|
2188
|
+
const decoder = new TextDecoder();
|
|
2189
|
+
try {
|
|
2190
|
+
for await (const chunk of res.body) {
|
|
2191
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
2192
|
+
}
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
if (!this.closed) {
|
|
2195
|
+
this.pushError(`SSE stream error: ${err.message}`);
|
|
2196
|
+
}
|
|
2197
|
+
} finally {
|
|
2198
|
+
this.markClosed();
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
handleEvent(type, data) {
|
|
2202
|
+
if (type === "endpoint") {
|
|
2203
|
+
if (this.postUrl) return;
|
|
2204
|
+
try {
|
|
2205
|
+
this.postUrl = new URL(data, this.url).toString();
|
|
2206
|
+
this.resolveEndpoint(this.postUrl);
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
this.failHandshake(`SSE endpoint event had bad URL "${data}": ${err.message}`);
|
|
2209
|
+
}
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (type === "message") {
|
|
2213
|
+
try {
|
|
2214
|
+
const parsed = JSON.parse(data);
|
|
2215
|
+
this.pushMessage(parsed);
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
failHandshake(reason) {
|
|
2222
|
+
this.rejectEndpoint(new Error(reason));
|
|
2223
|
+
this.pushError(reason);
|
|
2224
|
+
this.markClosed();
|
|
2225
|
+
}
|
|
2226
|
+
pushMessage(msg) {
|
|
2227
|
+
const waiter = this.waiters.shift();
|
|
2228
|
+
if (waiter) waiter(msg);
|
|
2229
|
+
else this.queue.push(msg);
|
|
2230
|
+
}
|
|
2231
|
+
pushError(message) {
|
|
2232
|
+
this.pushMessage({
|
|
2233
|
+
jsonrpc: "2.0",
|
|
2234
|
+
id: null,
|
|
2235
|
+
error: { code: -32e3, message }
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
markClosed() {
|
|
2239
|
+
if (this.closed) return;
|
|
2240
|
+
this.closed = true;
|
|
2241
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
|
|
2106
2245
|
// src/mcp/registry.ts
|
|
2107
2246
|
async function bridgeMcpTools(client, opts = {}) {
|
|
2108
2247
|
const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
|
|
@@ -2142,56 +2281,6 @@ function blockToString(block) {
|
|
|
2142
2281
|
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
2143
2282
|
}
|
|
2144
2283
|
|
|
2145
|
-
// src/config.ts
|
|
2146
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2147
|
-
import { homedir as homedir2 } from "os";
|
|
2148
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
2149
|
-
function defaultConfigPath() {
|
|
2150
|
-
return join2(homedir2(), ".reasonix", "config.json");
|
|
2151
|
-
}
|
|
2152
|
-
function readConfig(path = defaultConfigPath()) {
|
|
2153
|
-
try {
|
|
2154
|
-
const raw = readFileSync4(path, "utf8");
|
|
2155
|
-
const parsed = JSON.parse(raw);
|
|
2156
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
2157
|
-
} catch {
|
|
2158
|
-
}
|
|
2159
|
-
return {};
|
|
2160
|
-
}
|
|
2161
|
-
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2162
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
2163
|
-
writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2164
|
-
try {
|
|
2165
|
-
chmodSync2(path, 384);
|
|
2166
|
-
} catch {
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
function loadApiKey(path = defaultConfigPath()) {
|
|
2170
|
-
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
2171
|
-
return readConfig(path).apiKey;
|
|
2172
|
-
}
|
|
2173
|
-
function saveApiKey(key, path = defaultConfigPath()) {
|
|
2174
|
-
const cfg = readConfig(path);
|
|
2175
|
-
cfg.apiKey = key.trim();
|
|
2176
|
-
writeConfig(cfg, path);
|
|
2177
|
-
}
|
|
2178
|
-
function isPlausibleKey(key) {
|
|
2179
|
-
const trimmed = key.trim();
|
|
2180
|
-
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
2181
|
-
}
|
|
2182
|
-
function redactKey(key) {
|
|
2183
|
-
if (!key) return "";
|
|
2184
|
-
if (key.length <= 12) return "****";
|
|
2185
|
-
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// src/index.ts
|
|
2189
|
-
var VERSION = "0.3.0-alpha.3";
|
|
2190
|
-
|
|
2191
|
-
// src/cli/commands/chat.tsx
|
|
2192
|
-
import { render } from "ink";
|
|
2193
|
-
import React8, { useState as useState4 } from "react";
|
|
2194
|
-
|
|
2195
2284
|
// src/mcp/shell-split.ts
|
|
2196
2285
|
function shellSplit(input) {
|
|
2197
2286
|
const tokens = [];
|
|
@@ -2243,6 +2332,7 @@ function shellSplit(input) {
|
|
|
2243
2332
|
|
|
2244
2333
|
// src/mcp/spec.ts
|
|
2245
2334
|
var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
|
|
2335
|
+
var HTTP_URL = /^https?:\/\//i;
|
|
2246
2336
|
function parseMcpSpec(input) {
|
|
2247
2337
|
const trimmed = input.trim();
|
|
2248
2338
|
if (!trimmed) {
|
|
@@ -2250,15 +2340,71 @@ function parseMcpSpec(input) {
|
|
|
2250
2340
|
}
|
|
2251
2341
|
const nameMatch = NAME_PREFIX.exec(trimmed);
|
|
2252
2342
|
const name = nameMatch ? nameMatch[1] : null;
|
|
2253
|
-
const body = nameMatch ? nameMatch[2] : trimmed;
|
|
2343
|
+
const body = (nameMatch ? nameMatch[2] : trimmed).trim();
|
|
2344
|
+
if (!body) {
|
|
2345
|
+
throw new Error(`MCP spec has name but no command: ${input}`);
|
|
2346
|
+
}
|
|
2347
|
+
if (HTTP_URL.test(body)) {
|
|
2348
|
+
return { transport: "sse", name, url: body };
|
|
2349
|
+
}
|
|
2254
2350
|
const argv = shellSplit(body);
|
|
2255
2351
|
if (argv.length === 0) {
|
|
2256
2352
|
throw new Error(`MCP spec has name but no command: ${input}`);
|
|
2257
2353
|
}
|
|
2258
2354
|
const [command, ...args] = argv;
|
|
2259
|
-
return { name, command, args };
|
|
2355
|
+
return { transport: "stdio", name, command, args };
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/config.ts
|
|
2359
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2360
|
+
import { homedir as homedir2 } from "os";
|
|
2361
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
2362
|
+
function defaultConfigPath() {
|
|
2363
|
+
return join2(homedir2(), ".reasonix", "config.json");
|
|
2364
|
+
}
|
|
2365
|
+
function readConfig(path = defaultConfigPath()) {
|
|
2366
|
+
try {
|
|
2367
|
+
const raw = readFileSync4(path, "utf8");
|
|
2368
|
+
const parsed = JSON.parse(raw);
|
|
2369
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
2370
|
+
} catch {
|
|
2371
|
+
}
|
|
2372
|
+
return {};
|
|
2373
|
+
}
|
|
2374
|
+
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2375
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2376
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2377
|
+
try {
|
|
2378
|
+
chmodSync2(path, 384);
|
|
2379
|
+
} catch {
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function loadApiKey(path = defaultConfigPath()) {
|
|
2383
|
+
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
2384
|
+
return readConfig(path).apiKey;
|
|
2385
|
+
}
|
|
2386
|
+
function saveApiKey(key, path = defaultConfigPath()) {
|
|
2387
|
+
const cfg = readConfig(path);
|
|
2388
|
+
cfg.apiKey = key.trim();
|
|
2389
|
+
writeConfig(cfg, path);
|
|
2390
|
+
}
|
|
2391
|
+
function isPlausibleKey(key) {
|
|
2392
|
+
const trimmed = key.trim();
|
|
2393
|
+
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
2394
|
+
}
|
|
2395
|
+
function redactKey(key) {
|
|
2396
|
+
if (!key) return "";
|
|
2397
|
+
if (key.length <= 12) return "****";
|
|
2398
|
+
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
2260
2399
|
}
|
|
2261
2400
|
|
|
2401
|
+
// src/index.ts
|
|
2402
|
+
var VERSION = "0.3.0-alpha.4";
|
|
2403
|
+
|
|
2404
|
+
// src/cli/commands/chat.tsx
|
|
2405
|
+
import { render } from "ink";
|
|
2406
|
+
import React8, { useState as useState4 } from "react";
|
|
2407
|
+
|
|
2262
2408
|
// src/cli/ui/App.tsx
|
|
2263
2409
|
import { Box as Box6, Static, Text as Text6, useApp } from "ink";
|
|
2264
2410
|
import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
@@ -3016,13 +3162,14 @@ async function chatCommand(opts) {
|
|
|
3016
3162
|
try {
|
|
3017
3163
|
const spec = parseMcpSpec(raw);
|
|
3018
3164
|
const prefix = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3019
|
-
const transport = new StdioTransport({ command: spec.command, args: spec.args });
|
|
3165
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3020
3166
|
const mcp2 = new McpClient({ transport });
|
|
3021
3167
|
await mcp2.initialize();
|
|
3022
3168
|
const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix });
|
|
3023
3169
|
const label = spec.name ?? "anon";
|
|
3170
|
+
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
3024
3171
|
process.stderr.write(
|
|
3025
|
-
`\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${
|
|
3172
|
+
`\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
3026
3173
|
`
|
|
3027
3174
|
);
|
|
3028
3175
|
clients.push(mcp2);
|
|
@@ -3481,13 +3628,13 @@ async function runCommand(opts) {
|
|
|
3481
3628
|
try {
|
|
3482
3629
|
const spec = parseMcpSpec(raw);
|
|
3483
3630
|
const prefix2 = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3484
|
-
const
|
|
3485
|
-
|
|
3486
|
-
});
|
|
3631
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3632
|
+
const mcp2 = new McpClient({ transport });
|
|
3487
3633
|
await mcp2.initialize();
|
|
3488
3634
|
const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix2 });
|
|
3635
|
+
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
3489
3636
|
process.stderr.write(
|
|
3490
|
-
`\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${
|
|
3637
|
+
`\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
3491
3638
|
`
|
|
3492
3639
|
);
|
|
3493
3640
|
clients.push(mcp2);
|