reasonix 0.3.0-alpha.3 → 0.3.0-alpha.6

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
@@ -3,6 +3,49 @@
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
5
 
6
+ // src/config.ts
7
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { dirname, join } from "path";
10
+ function defaultConfigPath() {
11
+ return join(homedir(), ".reasonix", "config.json");
12
+ }
13
+ function readConfig(path = defaultConfigPath()) {
14
+ try {
15
+ const raw = readFileSync(path, "utf8");
16
+ const parsed = JSON.parse(raw);
17
+ if (parsed && typeof parsed === "object") return parsed;
18
+ } catch {
19
+ }
20
+ return {};
21
+ }
22
+ function writeConfig(cfg, path = defaultConfigPath()) {
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
25
+ try {
26
+ chmodSync(path, 384);
27
+ } catch {
28
+ }
29
+ }
30
+ function loadApiKey(path = defaultConfigPath()) {
31
+ if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
32
+ return readConfig(path).apiKey;
33
+ }
34
+ function saveApiKey(key, path = defaultConfigPath()) {
35
+ const cfg = readConfig(path);
36
+ cfg.apiKey = key.trim();
37
+ writeConfig(cfg, path);
38
+ }
39
+ function isPlausibleKey(key) {
40
+ const trimmed = key.trim();
41
+ return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
42
+ }
43
+ function redactKey(key) {
44
+ if (!key) return "";
45
+ if (key.length <= 12) return "****";
46
+ return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
47
+ }
48
+
6
49
  // src/client.ts
7
50
  import { createParser } from "eventsource-parser";
8
51
 
@@ -409,6 +452,201 @@ function resolveTemperatures(budget, custom) {
409
452
  return out;
410
453
  }
411
454
 
455
+ // src/repair/flatten.ts
456
+ function analyzeSchema(schema) {
457
+ if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
458
+ let leafCount = 0;
459
+ let maxDepth = 0;
460
+ walk(schema, 0, (depth, isLeaf) => {
461
+ if (isLeaf) leafCount++;
462
+ if (depth > maxDepth) maxDepth = depth;
463
+ });
464
+ return {
465
+ shouldFlatten: leafCount > 10 || maxDepth > 2,
466
+ leafCount,
467
+ maxDepth
468
+ };
469
+ }
470
+ function flattenSchema(schema) {
471
+ const flatProps = {};
472
+ const required = [];
473
+ collect("", schema, flatProps, required, true);
474
+ return {
475
+ type: "object",
476
+ properties: flatProps,
477
+ required
478
+ };
479
+ }
480
+ function nestArguments(flatArgs) {
481
+ const out = {};
482
+ for (const [key, value] of Object.entries(flatArgs)) {
483
+ setByPath(out, key.split("."), value);
484
+ }
485
+ return out;
486
+ }
487
+ function walk(schema, depth, visit) {
488
+ if (schema.type === "object" && schema.properties) {
489
+ for (const child of Object.values(schema.properties)) {
490
+ walk(child, depth + 1, visit);
491
+ }
492
+ return;
493
+ }
494
+ if (schema.type === "array" && schema.items) {
495
+ walk(schema.items, depth + 1, visit);
496
+ return;
497
+ }
498
+ visit(depth, true);
499
+ }
500
+ function collect(prefix, schema, out, required, isRootRequired) {
501
+ if (schema.type === "object" && schema.properties) {
502
+ const requiredSet = new Set(schema.required ?? []);
503
+ for (const [key, child] of Object.entries(schema.properties)) {
504
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
505
+ const childRequired = isRootRequired && requiredSet.has(key);
506
+ collect(nextPrefix, child, out, required, childRequired);
507
+ }
508
+ return;
509
+ }
510
+ out[prefix] = schema;
511
+ if (isRootRequired) required.push(prefix);
512
+ }
513
+ function setByPath(target, path, value) {
514
+ let cur = target;
515
+ for (let i = 0; i < path.length - 1; i++) {
516
+ const key = path[i];
517
+ if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
518
+ cur = cur[key];
519
+ }
520
+ cur[path[path.length - 1]] = value;
521
+ }
522
+
523
+ // src/tools.ts
524
+ var ToolRegistry = class {
525
+ _tools = /* @__PURE__ */ new Map();
526
+ _autoFlatten;
527
+ constructor(opts = {}) {
528
+ this._autoFlatten = opts.autoFlatten !== false;
529
+ }
530
+ register(def) {
531
+ if (!def.name) throw new Error("tool requires a name");
532
+ const internal = { ...def };
533
+ if (this._autoFlatten && def.parameters) {
534
+ const decision = analyzeSchema(def.parameters);
535
+ if (decision.shouldFlatten) {
536
+ internal.flatSchema = flattenSchema(def.parameters);
537
+ }
538
+ }
539
+ this._tools.set(def.name, internal);
540
+ return this;
541
+ }
542
+ has(name) {
543
+ return this._tools.has(name);
544
+ }
545
+ get(name) {
546
+ return this._tools.get(name);
547
+ }
548
+ get size() {
549
+ return this._tools.size;
550
+ }
551
+ /** True if a registered tool's schema was flattened for the model. */
552
+ wasFlattened(name) {
553
+ return Boolean(this._tools.get(name)?.flatSchema);
554
+ }
555
+ specs() {
556
+ return [...this._tools.values()].map((t) => ({
557
+ type: "function",
558
+ function: {
559
+ name: t.name,
560
+ description: t.description ?? "",
561
+ parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
562
+ }
563
+ }));
564
+ }
565
+ async dispatch(name, argumentsRaw) {
566
+ const tool = this._tools.get(name);
567
+ if (!tool) {
568
+ return JSON.stringify({ error: `unknown tool: ${name}` });
569
+ }
570
+ let args;
571
+ try {
572
+ args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
573
+ } catch (err) {
574
+ return JSON.stringify({
575
+ error: `invalid tool arguments JSON: ${err.message}`
576
+ });
577
+ }
578
+ if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
579
+ args = nestArguments(args);
580
+ }
581
+ try {
582
+ const result = await tool.fn(args);
583
+ return typeof result === "string" ? result : JSON.stringify(result);
584
+ } catch (err) {
585
+ return JSON.stringify({
586
+ error: `${err.name}: ${err.message}`
587
+ });
588
+ }
589
+ }
590
+ };
591
+ function hasDotKey(obj) {
592
+ for (const k of Object.keys(obj)) {
593
+ if (k.includes(".")) return true;
594
+ }
595
+ return false;
596
+ }
597
+
598
+ // src/mcp/registry.ts
599
+ var DEFAULT_MAX_RESULT_CHARS = 32e3;
600
+ async function bridgeMcpTools(client, opts = {}) {
601
+ const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
602
+ const prefix = opts.namePrefix ?? "";
603
+ const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS;
604
+ const result = { registry, registeredNames: [], skipped: [] };
605
+ const listed = await client.listTools();
606
+ for (const mcpTool of listed.tools) {
607
+ if (!mcpTool.name) {
608
+ result.skipped.push({ name: "?", reason: "empty tool name" });
609
+ continue;
610
+ }
611
+ const registeredName = `${prefix}${mcpTool.name}`;
612
+ registry.register({
613
+ name: registeredName,
614
+ description: mcpTool.description ?? "",
615
+ parameters: mcpTool.inputSchema,
616
+ fn: async (args) => {
617
+ const toolResult = await client.callTool(mcpTool.name, args);
618
+ return flattenMcpResult(toolResult, { maxChars: maxResultChars });
619
+ }
620
+ });
621
+ result.registeredNames.push(registeredName);
622
+ }
623
+ return result;
624
+ }
625
+ function flattenMcpResult(result, opts = {}) {
626
+ const parts = result.content.map(blockToString);
627
+ const joined = parts.join("\n").trim();
628
+ const prefixed = result.isError ? `ERROR: ${joined || "(no error message from server)"}` : joined;
629
+ return opts.maxChars ? truncateForModel(prefixed, opts.maxChars) : prefixed;
630
+ }
631
+ function truncateForModel(s, maxChars) {
632
+ if (s.length <= maxChars) return s;
633
+ const tailBudget = Math.min(1024, Math.floor(maxChars * 0.1));
634
+ const headBudget = Math.max(0, maxChars - tailBudget);
635
+ const head = s.slice(0, headBudget);
636
+ const tail = s.slice(-tailBudget);
637
+ const dropped = s.length - head.length - tail.length;
638
+ return `${head}
639
+
640
+ [\u2026truncated ${dropped} chars \u2014 raise BridgeOptions.maxResultChars, or call the tool with a narrower scope (filter, head, pagination)\u2026]
641
+
642
+ ${tail}`;
643
+ }
644
+ function blockToString(block) {
645
+ if (block.type === "text") return block.text;
646
+ if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
647
+ return `[unknown block: ${JSON.stringify(block)}]`;
648
+ }
649
+
412
650
  // src/memory.ts
413
651
  import { createHash } from "crypto";
414
652
  var ImmutablePrefix = class {
@@ -660,74 +898,6 @@ function repairTruncatedJson(input) {
660
898
  }
661
899
  }
662
900
 
663
- // src/repair/flatten.ts
664
- function analyzeSchema(schema) {
665
- if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
666
- let leafCount = 0;
667
- let maxDepth = 0;
668
- walk(schema, 0, (depth, isLeaf) => {
669
- if (isLeaf) leafCount++;
670
- if (depth > maxDepth) maxDepth = depth;
671
- });
672
- return {
673
- shouldFlatten: leafCount > 10 || maxDepth > 2,
674
- leafCount,
675
- maxDepth
676
- };
677
- }
678
- function flattenSchema(schema) {
679
- const flatProps = {};
680
- const required = [];
681
- collect("", schema, flatProps, required, true);
682
- return {
683
- type: "object",
684
- properties: flatProps,
685
- required
686
- };
687
- }
688
- function nestArguments(flatArgs) {
689
- const out = {};
690
- for (const [key, value] of Object.entries(flatArgs)) {
691
- setByPath(out, key.split("."), value);
692
- }
693
- return out;
694
- }
695
- function walk(schema, depth, visit) {
696
- if (schema.type === "object" && schema.properties) {
697
- for (const child of Object.values(schema.properties)) {
698
- walk(child, depth + 1, visit);
699
- }
700
- return;
701
- }
702
- if (schema.type === "array" && schema.items) {
703
- walk(schema.items, depth + 1, visit);
704
- return;
705
- }
706
- visit(depth, true);
707
- }
708
- function collect(prefix, schema, out, required, isRootRequired) {
709
- if (schema.type === "object" && schema.properties) {
710
- const requiredSet = new Set(schema.required ?? []);
711
- for (const [key, child] of Object.entries(schema.properties)) {
712
- const nextPrefix = prefix ? `${prefix}.${key}` : key;
713
- const childRequired = isRootRequired && requiredSet.has(key);
714
- collect(nextPrefix, child, out, required, childRequired);
715
- }
716
- return;
717
- }
718
- out[prefix] = schema;
719
- if (isRootRequired) required.push(prefix);
720
- }
721
- function setByPath(target, path, value) {
722
- let cur = target;
723
- for (let i = 0; i < path.length - 1; i++) {
724
- const key = path[i];
725
- if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
726
- cur = cur[key];
727
- }
728
- cur[path[path.length - 1]] = value;
729
- }
730
-
731
901
  // src/repair/index.ts
732
902
  var ToolCallRepair = class {
733
903
  storm;
@@ -786,21 +956,21 @@ function signature2(call) {
786
956
  // src/session.ts
787
957
  import {
788
958
  appendFileSync,
789
- chmodSync,
959
+ chmodSync as chmodSync2,
790
960
  existsSync,
791
- mkdirSync,
792
- readFileSync,
961
+ mkdirSync as mkdirSync2,
962
+ readFileSync as readFileSync2,
793
963
  readdirSync,
794
964
  statSync,
795
965
  unlinkSync
796
966
  } from "fs";
797
- import { homedir } from "os";
798
- import { dirname, join } from "path";
967
+ import { homedir as homedir2 } from "os";
968
+ import { dirname as dirname2, join as join2 } from "path";
799
969
  function sessionsDir() {
800
- return join(homedir(), ".reasonix", "sessions");
970
+ return join2(homedir2(), ".reasonix", "sessions");
801
971
  }
802
972
  function sessionPath(name) {
803
- return join(sessionsDir(), `${sanitizeName(name)}.jsonl`);
973
+ return join2(sessionsDir(), `${sanitizeName(name)}.jsonl`);
804
974
  }
805
975
  function sanitizeName(name) {
806
976
  const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
@@ -810,7 +980,7 @@ function loadSessionMessages(name) {
810
980
  const path = sessionPath(name);
811
981
  if (!existsSync(path)) return [];
812
982
  try {
813
- const raw = readFileSync(path, "utf8");
983
+ const raw = readFileSync2(path, "utf8");
814
984
  const out = [];
815
985
  for (const line of raw.split(/\r?\n/)) {
816
986
  const trimmed = line.trim();
@@ -828,11 +998,11 @@ function loadSessionMessages(name) {
828
998
  }
829
999
  function appendSessionMessage(name, message) {
830
1000
  const path = sessionPath(name);
831
- mkdirSync(dirname(path), { recursive: true });
1001
+ mkdirSync2(dirname2(path), { recursive: true });
832
1002
  appendFileSync(path, `${JSON.stringify(message)}
833
1003
  `, "utf8");
834
1004
  try {
835
- chmodSync(path, 384);
1005
+ chmodSync2(path, 384);
836
1006
  } catch {
837
1007
  }
838
1008
  }
@@ -842,7 +1012,7 @@ function listSessions() {
842
1012
  try {
843
1013
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
844
1014
  return files.map((file) => {
845
- const path = join(dir, file);
1015
+ const path = join2(dir, file);
846
1016
  const stat = statSync(path);
847
1017
  const name = file.replace(/\.jsonl$/, "");
848
1018
  const messageCount = countLines(path);
@@ -863,7 +1033,7 @@ function deleteSession(name) {
863
1033
  }
864
1034
  function countLines(path) {
865
1035
  try {
866
- const raw = readFileSync(path, "utf8");
1036
+ const raw = readFileSync2(path, "utf8");
867
1037
  return raw.split(/\r?\n/).filter((l) => l.trim()).length;
868
1038
  } catch {
869
1039
  return 0;
@@ -933,81 +1103,6 @@ function round(n, digits) {
933
1103
  return Math.round(n * f) / f;
934
1104
  }
935
1105
 
936
- // src/tools.ts
937
- var ToolRegistry = class {
938
- _tools = /* @__PURE__ */ new Map();
939
- _autoFlatten;
940
- constructor(opts = {}) {
941
- this._autoFlatten = opts.autoFlatten !== false;
942
- }
943
- register(def) {
944
- if (!def.name) throw new Error("tool requires a name");
945
- const internal = { ...def };
946
- if (this._autoFlatten && def.parameters) {
947
- const decision = analyzeSchema(def.parameters);
948
- if (decision.shouldFlatten) {
949
- internal.flatSchema = flattenSchema(def.parameters);
950
- }
951
- }
952
- this._tools.set(def.name, internal);
953
- return this;
954
- }
955
- has(name) {
956
- return this._tools.has(name);
957
- }
958
- get(name) {
959
- return this._tools.get(name);
960
- }
961
- get size() {
962
- return this._tools.size;
963
- }
964
- /** True if a registered tool's schema was flattened for the model. */
965
- wasFlattened(name) {
966
- return Boolean(this._tools.get(name)?.flatSchema);
967
- }
968
- specs() {
969
- return [...this._tools.values()].map((t) => ({
970
- type: "function",
971
- function: {
972
- name: t.name,
973
- description: t.description ?? "",
974
- parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
975
- }
976
- }));
977
- }
978
- async dispatch(name, argumentsRaw) {
979
- const tool = this._tools.get(name);
980
- if (!tool) {
981
- return JSON.stringify({ error: `unknown tool: ${name}` });
982
- }
983
- let args;
984
- try {
985
- args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
986
- } catch (err) {
987
- return JSON.stringify({
988
- error: `invalid tool arguments JSON: ${err.message}`
989
- });
990
- }
991
- if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
992
- args = nestArguments(args);
993
- }
994
- try {
995
- const result = await tool.fn(args);
996
- return typeof result === "string" ? result : JSON.stringify(result);
997
- } catch (err) {
998
- return JSON.stringify({
999
- error: `${err.name}: ${err.message}`
1000
- });
1001
- }
1002
- }
1003
- };
1004
- function hasDotKey(obj) {
1005
- for (const k of Object.keys(obj)) {
1006
- if (k.includes(".")) return true;
1007
- }
1008
- return false;
1009
- }
1010
-
1011
1106
  // src/loop.ts
1012
1107
  var CacheFirstLoop = class {
1013
1108
  client;
@@ -1055,8 +1150,18 @@ var CacheFirstLoop = class {
1055
1150
  this.sessionName = opts.session ?? null;
1056
1151
  if (this.sessionName) {
1057
1152
  const prior = loadSessionMessages(this.sessionName);
1058
- for (const msg of prior) this.log.append(msg);
1059
- this.resumedMessageCount = prior.length;
1153
+ const { messages, healedCount, healedFrom } = healLoadedMessages(
1154
+ prior,
1155
+ DEFAULT_MAX_RESULT_CHARS
1156
+ );
1157
+ for (const msg of messages) this.log.append(msg);
1158
+ this.resumedMessageCount = messages.length;
1159
+ if (healedCount > 0) {
1160
+ process.stderr.write(
1161
+ `\u25B8 session "${this.sessionName}": healed ${healedCount} oversized tool result(s) (was ${healedFrom.toLocaleString()} chars total). Old payloads were truncated to fit DeepSeek's context window; the conversation is preserved.
1162
+ `
1163
+ );
1164
+ }
1060
1165
  } else {
1061
1166
  this.resumedMessageCount = 0;
1062
1167
  }
@@ -1250,7 +1355,7 @@ var CacheFirstLoop = class {
1250
1355
  turn: this._turn,
1251
1356
  role: "error",
1252
1357
  content: "",
1253
- error: err.message
1358
+ error: formatLoopError(err)
1254
1359
  };
1255
1360
  return;
1256
1361
  }
@@ -1323,14 +1428,36 @@ function summarizeBranch(chosen, samples) {
1323
1428
  temperatures: samples.map((s) => s.temperature)
1324
1429
  };
1325
1430
  }
1431
+ function healLoadedMessages(messages, maxChars) {
1432
+ let healedCount = 0;
1433
+ let healedFrom = 0;
1434
+ const out = messages.map((msg) => {
1435
+ if (msg.role !== "tool") return msg;
1436
+ const content = typeof msg.content === "string" ? msg.content : "";
1437
+ if (content.length <= maxChars) return msg;
1438
+ healedCount += 1;
1439
+ healedFrom += content.length;
1440
+ return { ...msg, content: truncateForModel(content, maxChars) };
1441
+ });
1442
+ return { messages: out, healedCount, healedFrom };
1443
+ }
1444
+ function formatLoopError(err) {
1445
+ const msg = err.message ?? "";
1446
+ if (msg.includes("maximum context length")) {
1447
+ const reqMatch = msg.match(/requested\s+(\d+)\s+tokens/);
1448
+ const requested = reqMatch ? `${Number(reqMatch[1]).toLocaleString()} tokens` : "too many tokens";
1449
+ return `Context overflow (DeepSeek 400): session history is ${requested}, past the 131,072-token limit. Usually this means a single tool call returned a huge payload. v0.3.0-alpha.6+ caps new tool results at 32k chars, AND auto-heals oversized history on session load \u2014 restart Reasonix and this session should come back trimmed. If it still overflows, run /forget (delete the session) or /clear (drop the displayed history) to start fresh.`;
1450
+ }
1451
+ return msg;
1452
+ }
1326
1453
 
1327
1454
  // src/env.ts
1328
- import { readFileSync as readFileSync2 } from "fs";
1455
+ import { readFileSync as readFileSync3 } from "fs";
1329
1456
  import { resolve } from "path";
1330
1457
  function loadDotenv(path = ".env") {
1331
1458
  let raw;
1332
1459
  try {
1333
- raw = readFileSync2(resolve(process.cwd(), path), "utf8");
1460
+ raw = readFileSync3(resolve(process.cwd(), path), "utf8");
1334
1461
  } catch {
1335
1462
  return;
1336
1463
  }
@@ -1349,7 +1476,7 @@ function loadDotenv(path = ".env") {
1349
1476
  }
1350
1477
 
1351
1478
  // src/transcript.ts
1352
- import { createWriteStream, readFileSync as readFileSync3 } from "fs";
1479
+ import { createWriteStream, readFileSync as readFileSync4 } from "fs";
1353
1480
  function recordFromLoopEvent(ev, extra) {
1354
1481
  const rec = {
1355
1482
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1400,7 +1527,7 @@ function openTranscriptFile(path, meta) {
1400
1527
  return stream;
1401
1528
  }
1402
1529
  function readTranscript(path) {
1403
- const raw = readFileSync3(path, "utf8");
1530
+ const raw = readFileSync4(path, "utf8");
1404
1531
  return parseTranscript(raw);
1405
1532
  }
1406
1533
  function isPlanStateEmptyShape(s) {
@@ -2103,94 +2230,144 @@ function quoteArg(s, windows) {
2103
2230
  return `"${s.replace(/"/g, '""')}"`;
2104
2231
  }
2105
2232
 
2106
- // src/mcp/registry.ts
2107
- async function bridgeMcpTools(client, opts = {}) {
2108
- const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
2109
- const prefix = opts.namePrefix ?? "";
2110
- const result = { registry, registeredNames: [], skipped: [] };
2111
- const listed = await client.listTools();
2112
- for (const mcpTool of listed.tools) {
2113
- if (!mcpTool.name) {
2114
- result.skipped.push({ name: "?", reason: "empty tool name" });
2115
- continue;
2233
+ // src/mcp/sse.ts
2234
+ import { createParser as createParser2 } from "eventsource-parser";
2235
+ var SseTransport = class {
2236
+ url;
2237
+ headers;
2238
+ queue = [];
2239
+ waiters = [];
2240
+ controller = new AbortController();
2241
+ closed = false;
2242
+ postUrl = null;
2243
+ endpointReady;
2244
+ resolveEndpoint;
2245
+ rejectEndpoint;
2246
+ constructor(opts) {
2247
+ this.url = opts.url;
2248
+ this.headers = opts.headers ?? {};
2249
+ this.endpointReady = new Promise((resolve2, reject) => {
2250
+ this.resolveEndpoint = resolve2;
2251
+ this.rejectEndpoint = reject;
2252
+ });
2253
+ this.endpointReady.catch(() => void 0);
2254
+ void this.runStream();
2255
+ }
2256
+ async send(message) {
2257
+ if (this.closed) throw new Error("MCP SSE transport is closed");
2258
+ const postUrl = await this.endpointReady;
2259
+ const res = await fetch(postUrl, {
2260
+ method: "POST",
2261
+ headers: { "content-type": "application/json", ...this.headers },
2262
+ body: JSON.stringify(message),
2263
+ signal: this.controller.signal
2264
+ });
2265
+ await res.arrayBuffer().catch(() => void 0);
2266
+ if (!res.ok) {
2267
+ throw new Error(`MCP SSE POST ${postUrl} failed: ${res.status} ${res.statusText}`);
2268
+ }
2269
+ }
2270
+ async *messages() {
2271
+ while (true) {
2272
+ if (this.queue.length > 0) {
2273
+ yield this.queue.shift();
2274
+ continue;
2275
+ }
2276
+ if (this.closed) return;
2277
+ const next = await new Promise((resolve2) => {
2278
+ this.waiters.push(resolve2);
2279
+ });
2280
+ if (next === null) return;
2281
+ yield next;
2282
+ }
2283
+ }
2284
+ async close() {
2285
+ if (this.closed) return;
2286
+ this.closed = true;
2287
+ while (this.waiters.length > 0) this.waiters.shift()(null);
2288
+ this.rejectEndpoint(new Error("MCP SSE transport closed before endpoint was ready"));
2289
+ try {
2290
+ this.controller.abort();
2291
+ } catch {
2292
+ }
2293
+ }
2294
+ // ---------- internals ----------
2295
+ async runStream() {
2296
+ let res;
2297
+ try {
2298
+ res = await fetch(this.url, {
2299
+ method: "GET",
2300
+ headers: { accept: "text/event-stream", ...this.headers },
2301
+ signal: this.controller.signal
2302
+ });
2303
+ } catch (err) {
2304
+ this.failHandshake(`SSE connect to ${this.url} failed: ${err.message}`);
2305
+ return;
2306
+ }
2307
+ if (!res.ok || !res.body) {
2308
+ await res.body?.cancel().catch(() => void 0);
2309
+ this.failHandshake(`SSE handshake ${this.url} \u2192 ${res.status} ${res.statusText}`);
2310
+ return;
2311
+ }
2312
+ const parser = createParser2({
2313
+ onEvent: (ev) => this.handleEvent(ev.event ?? "message", ev.data)
2314
+ });
2315
+ const decoder = new TextDecoder();
2316
+ try {
2317
+ for await (const chunk of res.body) {
2318
+ parser.feed(decoder.decode(chunk, { stream: true }));
2319
+ }
2320
+ } catch (err) {
2321
+ if (!this.closed) {
2322
+ this.pushError(`SSE stream error: ${err.message}`);
2323
+ }
2324
+ } finally {
2325
+ this.markClosed();
2326
+ }
2327
+ }
2328
+ handleEvent(type, data) {
2329
+ if (type === "endpoint") {
2330
+ if (this.postUrl) return;
2331
+ try {
2332
+ this.postUrl = new URL(data, this.url).toString();
2333
+ this.resolveEndpoint(this.postUrl);
2334
+ } catch (err) {
2335
+ this.failHandshake(`SSE endpoint event had bad URL "${data}": ${err.message}`);
2336
+ }
2337
+ return;
2116
2338
  }
2117
- const registeredName = `${prefix}${mcpTool.name}`;
2118
- registry.register({
2119
- name: registeredName,
2120
- description: mcpTool.description ?? "",
2121
- parameters: mcpTool.inputSchema,
2122
- fn: async (args) => {
2123
- const toolResult = await client.callTool(mcpTool.name, args);
2124
- return flattenMcpResult(toolResult);
2339
+ if (type === "message") {
2340
+ try {
2341
+ const parsed = JSON.parse(data);
2342
+ this.pushMessage(parsed);
2343
+ } catch {
2125
2344
  }
2126
- });
2127
- result.registeredNames.push(registeredName);
2345
+ return;
2346
+ }
2128
2347
  }
2129
- return result;
2130
- }
2131
- function flattenMcpResult(result) {
2132
- const parts = result.content.map(blockToString);
2133
- const joined = parts.join("\n").trim();
2134
- if (result.isError) {
2135
- return `ERROR: ${joined || "(no error message from server)"}`;
2348
+ failHandshake(reason) {
2349
+ this.rejectEndpoint(new Error(reason));
2350
+ this.pushError(reason);
2351
+ this.markClosed();
2136
2352
  }
2137
- return joined;
2138
- }
2139
- function blockToString(block) {
2140
- if (block.type === "text") return block.text;
2141
- if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
2142
- return `[unknown block: ${JSON.stringify(block)}]`;
2143
- }
2144
-
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 {
2353
+ pushMessage(msg) {
2354
+ const waiter = this.waiters.shift();
2355
+ if (waiter) waiter(msg);
2356
+ else this.queue.push(msg);
2158
2357
  }
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 {
2358
+ pushError(message) {
2359
+ this.pushMessage({
2360
+ jsonrpc: "2.0",
2361
+ id: null,
2362
+ error: { code: -32e3, message }
2363
+ });
2167
2364
  }
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";
2365
+ markClosed() {
2366
+ if (this.closed) return;
2367
+ this.closed = true;
2368
+ while (this.waiters.length > 0) this.waiters.shift()(null);
2369
+ }
2370
+ };
2194
2371
 
2195
2372
  // src/mcp/shell-split.ts
2196
2373
  function shellSplit(input) {
@@ -2243,6 +2420,7 @@ function shellSplit(input) {
2243
2420
 
2244
2421
  // src/mcp/spec.ts
2245
2422
  var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
2423
+ var HTTP_URL = /^https?:\/\//i;
2246
2424
  function parseMcpSpec(input) {
2247
2425
  const trimmed = input.trim();
2248
2426
  if (!trimmed) {
@@ -2250,15 +2428,28 @@ function parseMcpSpec(input) {
2250
2428
  }
2251
2429
  const nameMatch = NAME_PREFIX.exec(trimmed);
2252
2430
  const name = nameMatch ? nameMatch[1] : null;
2253
- const body = nameMatch ? nameMatch[2] : trimmed;
2431
+ const body = (nameMatch ? nameMatch[2] : trimmed).trim();
2432
+ if (!body) {
2433
+ throw new Error(`MCP spec has name but no command: ${input}`);
2434
+ }
2435
+ if (HTTP_URL.test(body)) {
2436
+ return { transport: "sse", name, url: body };
2437
+ }
2254
2438
  const argv = shellSplit(body);
2255
2439
  if (argv.length === 0) {
2256
2440
  throw new Error(`MCP spec has name but no command: ${input}`);
2257
2441
  }
2258
2442
  const [command, ...args] = argv;
2259
- return { name, command, args };
2443
+ return { transport: "stdio", name, command, args };
2260
2444
  }
2261
2445
 
2446
+ // src/index.ts
2447
+ var VERSION = "0.3.0-alpha.6";
2448
+
2449
+ // src/cli/commands/chat.tsx
2450
+ import { render } from "ink";
2451
+ import React8, { useState as useState4 } from "react";
2452
+
2262
2453
  // src/cli/ui/App.tsx
2263
2454
  import { Box as Box6, Static, Text as Text6, useApp } from "ink";
2264
2455
  import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
@@ -2592,7 +2783,7 @@ function parseSlash(text) {
2592
2783
  if (!cmd) return null;
2593
2784
  return { cmd, args: parts.slice(1) };
2594
2785
  }
2595
- function handleSlash(cmd, args, loop) {
2786
+ function handleSlash(cmd, args, loop, ctx = {}) {
2596
2787
  switch (cmd) {
2597
2788
  case "exit":
2598
2789
  case "quit":
@@ -2610,6 +2801,8 @@ function handleSlash(cmd, args, loop) {
2610
2801
  " /model <id> deepseek-chat or deepseek-reasoner",
2611
2802
  " /harvest [on|off] Pillar 2: structured plan-state extraction",
2612
2803
  " /branch <N|off> run N parallel samples (N>=2), pick most confident",
2804
+ " /mcp list MCP servers + tools attached to this session",
2805
+ " /setup (exit + reconfigure) \u2192 run `reasonix setup`",
2613
2806
  " /sessions list saved sessions (current is marked with \u25B8)",
2614
2807
  " /forget delete the current session from disk",
2615
2808
  " /clear clear displayed history (log + session kept)",
@@ -2625,6 +2818,32 @@ function handleSlash(cmd, args, loop) {
2625
2818
  " reasonix chat --no-session disable persistence for this run"
2626
2819
  ].join("\n")
2627
2820
  };
2821
+ case "mcp": {
2822
+ const specs = ctx.mcpSpecs ?? [];
2823
+ const toolSpecs = loop.prefix.toolSpecs ?? [];
2824
+ if (specs.length === 0 && toolSpecs.length === 0) {
2825
+ return {
2826
+ info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
2827
+ };
2828
+ }
2829
+ const lines = [];
2830
+ if (specs.length > 0) {
2831
+ lines.push(`MCP servers (${specs.length}):`);
2832
+ for (const spec of specs) lines.push(` \xB7 ${spec}`);
2833
+ lines.push("");
2834
+ }
2835
+ if (toolSpecs.length > 0) {
2836
+ lines.push(`Tools in registry (${toolSpecs.length}):`);
2837
+ for (const t of toolSpecs) lines.push(` \xB7 ${t.function.name}`);
2838
+ }
2839
+ lines.push("");
2840
+ lines.push("To change this set, exit and run `reasonix setup`.");
2841
+ return { info: lines.join("\n") };
2842
+ }
2843
+ case "setup":
2844
+ return {
2845
+ info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
2846
+ };
2628
2847
  case "sessions": {
2629
2848
  const items = listSessions();
2630
2849
  if (items.length === 0) {
@@ -2714,7 +2933,16 @@ function handleSlash(cmd, args, loop) {
2714
2933
 
2715
2934
  // src/cli/ui/App.tsx
2716
2935
  var FLUSH_INTERVAL_MS = 60;
2717
- function App({ model, system, transcript, harvest: harvest2, branch, session, tools }) {
2936
+ function App({
2937
+ model,
2938
+ system,
2939
+ transcript,
2940
+ harvest: harvest2,
2941
+ branch,
2942
+ session,
2943
+ tools,
2944
+ mcpSpecs
2945
+ }) {
2718
2946
  const { exit } = useApp();
2719
2947
  const [historical, setHistorical] = useState2([]);
2720
2948
  const [streaming, setStreaming] = useState2(null);
@@ -2802,7 +3030,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
2802
3030
  setInput("");
2803
3031
  const slash = parseSlash(text);
2804
3032
  if (slash) {
2805
- const result = handleSlash(slash.cmd, slash.args, loop);
3033
+ const result = handleSlash(slash.cmd, slash.args, loop, { mcpSpecs });
2806
3034
  if (result.exit) {
2807
3035
  transcriptRef.current?.end();
2808
3036
  exit();
@@ -2913,7 +3141,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
2913
3141
  setBusy(false);
2914
3142
  }
2915
3143
  },
2916
- [busy, exit, loop, writeTranscript]
3144
+ [busy, exit, loop, mcpSpecs, writeTranscript]
2917
3145
  );
2918
3146
  return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
2919
3147
  StatsPanel,
@@ -2927,7 +3155,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
2927
3155
  ), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, null));
2928
3156
  }
2929
3157
  function CommandStrip() {
2930
- return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /sessions \xB7 /model \xB7 /harvest \xB7 /branch \xB7 /clear \xB7 /exit"));
3158
+ return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"));
2931
3159
  }
2932
3160
  function describeRepair(repair) {
2933
3161
  const parts = [];
@@ -2977,7 +3205,7 @@ function Setup({ onReady }) {
2977
3205
  }
2978
3206
 
2979
3207
  // src/cli/commands/chat.tsx
2980
- function Root({ initialKey, tools, ...appProps }) {
3208
+ function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
2981
3209
  const [key, setKey] = useState4(initialKey);
2982
3210
  if (!key) {
2983
3211
  return /* @__PURE__ */ React8.createElement(
@@ -3000,43 +3228,56 @@ function Root({ initialKey, tools, ...appProps }) {
3000
3228
  harvest: appProps.harvest,
3001
3229
  branch: appProps.branch,
3002
3230
  session: appProps.session,
3003
- tools
3231
+ tools,
3232
+ mcpSpecs
3004
3233
  }
3005
3234
  );
3006
3235
  }
3007
3236
  async function chatCommand(opts) {
3008
3237
  loadDotenv();
3009
3238
  const initialKey = loadApiKey();
3010
- const mcpSpecs = opts.mcp ?? [];
3239
+ const requestedSpecs = opts.mcp ?? [];
3011
3240
  const clients = [];
3241
+ const successfulSpecs = [];
3242
+ const failedSpecs = [];
3012
3243
  let tools;
3013
- if (mcpSpecs.length > 0) {
3244
+ if (requestedSpecs.length > 0) {
3014
3245
  tools = new ToolRegistry();
3015
- for (const raw of mcpSpecs) {
3246
+ for (const raw of requestedSpecs) {
3016
3247
  try {
3017
3248
  const spec = parseMcpSpec(raw);
3018
- const prefix = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3019
- const transport = new StdioTransport({ command: spec.command, args: spec.args });
3249
+ const prefix = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3250
+ const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
3020
3251
  const mcp2 = new McpClient({ transport });
3021
3252
  await mcp2.initialize();
3022
3253
  const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix });
3023
3254
  const label = spec.name ?? "anon";
3255
+ const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
3024
3256
  process.stderr.write(
3025
- `\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${spec.command} ${spec.args.join(" ")}
3257
+ `\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
3026
3258
  `
3027
3259
  );
3028
3260
  clients.push(mcp2);
3261
+ successfulSpecs.push(raw);
3029
3262
  } catch (err) {
3030
- process.stderr.write(`MCP setup failed for "${raw}": ${err.message}
3031
- `);
3032
- for (const c of clients) await c.close();
3033
- process.exit(1);
3263
+ const reason = err.message;
3264
+ failedSpecs.push({ spec: raw, reason });
3265
+ process.stderr.write(
3266
+ `\u25B8 MCP setup SKIPPED for "${raw}": ${reason}
3267
+ \u2192 this server will not be available this session. Run \`reasonix setup\` to remove it, or fix the underlying issue (missing npm package, network, etc.).
3268
+ `
3269
+ );
3034
3270
  }
3035
3271
  }
3272
+ if (successfulSpecs.length === 0) {
3273
+ tools = void 0;
3274
+ }
3036
3275
  }
3037
- const { waitUntilExit } = render(/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, ...opts }), {
3038
- exitOnCtrlC: true
3039
- });
3276
+ const mcpSpecs = successfulSpecs;
3277
+ const { waitUntilExit } = render(
3278
+ /* @__PURE__ */ React8.createElement(Root, { initialKey, tools, mcpSpecs, ...opts }),
3279
+ { exitOnCtrlC: true }
3280
+ );
3040
3281
  try {
3041
3282
  await waitUntilExit();
3042
3283
  } finally {
@@ -3215,11 +3456,6 @@ var MCP_CATALOG = [
3215
3456
  userArgs: "<dir>",
3216
3457
  note: "the directory is a hard sandbox \u2014 the server refuses access outside it"
3217
3458
  },
3218
- {
3219
- name: "fetch",
3220
- summary: "fetch URLs (markdown-friendly extraction, not a full browser)",
3221
- package: "@modelcontextprotocol/server-fetch"
3222
- },
3223
3459
  {
3224
3460
  name: "memory",
3225
3461
  summary: "persistent key-value memory across sessions",
@@ -3231,12 +3467,6 @@ var MCP_CATALOG = [
3231
3467
  package: "@modelcontextprotocol/server-github",
3232
3468
  note: "set GITHUB_PERSONAL_ACCESS_TOKEN in your env before spawning"
3233
3469
  },
3234
- {
3235
- name: "sqlite",
3236
- summary: "read/write a sqlite database file",
3237
- package: "@modelcontextprotocol/server-sqlite",
3238
- userArgs: "<db.sqlite>"
3239
- },
3240
3470
  {
3241
3471
  name: "puppeteer",
3242
3472
  summary: "browser automation \u2014 take screenshots, click, type",
@@ -3472,32 +3702,36 @@ async function runCommand(opts) {
3472
3702
  loadDotenv();
3473
3703
  const apiKey = await ensureApiKey();
3474
3704
  process.env.DEEPSEEK_API_KEY = apiKey;
3475
- const mcpSpecs = opts.mcp ?? [];
3705
+ const requestedSpecs = opts.mcp ?? [];
3476
3706
  const clients = [];
3477
3707
  let tools;
3478
- if (mcpSpecs.length > 0) {
3708
+ let successCount = 0;
3709
+ if (requestedSpecs.length > 0) {
3479
3710
  tools = new ToolRegistry();
3480
- for (const raw of mcpSpecs) {
3711
+ for (const raw of requestedSpecs) {
3481
3712
  try {
3482
3713
  const spec = parseMcpSpec(raw);
3483
- const prefix2 = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3484
- const mcp2 = new McpClient({
3485
- transport: new StdioTransport({ command: spec.command, args: spec.args })
3486
- });
3714
+ const prefix2 = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3715
+ const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
3716
+ const mcp2 = new McpClient({ transport });
3487
3717
  await mcp2.initialize();
3488
3718
  const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix2 });
3719
+ const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
3489
3720
  process.stderr.write(
3490
- `\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${spec.command} ${spec.args.join(" ")}
3721
+ `\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
3491
3722
  `
3492
3723
  );
3493
3724
  clients.push(mcp2);
3725
+ successCount++;
3494
3726
  } catch (err) {
3495
- process.stderr.write(`MCP setup failed for "${raw}": ${err.message}
3496
- `);
3497
- for (const c of clients) await c.close();
3498
- process.exit(1);
3727
+ process.stderr.write(
3728
+ `\u25B8 MCP setup SKIPPED for "${raw}": ${err.message}
3729
+ \u2192 run \`reasonix setup\` to remove broken entries from your saved config.
3730
+ `
3731
+ );
3499
3732
  }
3500
3733
  }
3734
+ if (successCount === 0) tools = void 0;
3501
3735
  }
3502
3736
  const client = new DeepSeekClient();
3503
3737
  const prefix = new ImmutablePrefix({
@@ -3637,6 +3871,404 @@ function truncate4(s, max) {
3637
3871
  return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
3638
3872
  }
3639
3873
 
3874
+ // src/cli/commands/setup.tsx
3875
+ import { render as render4 } from "ink";
3876
+ import React16 from "react";
3877
+
3878
+ // src/cli/ui/Wizard.tsx
3879
+ import { Box as Box12, Text as Text12, useApp as useApp5, useInput as useInput4 } from "ink";
3880
+ import TextInput3 from "ink-text-input";
3881
+ import React15, { useState as useState8 } from "react";
3882
+
3883
+ // src/cli/ui/Select.tsx
3884
+ import { Box as Box11, Text as Text11, useInput as useInput3 } from "ink";
3885
+ import React14, { useState as useState7 } from "react";
3886
+ function SingleSelect({
3887
+ items,
3888
+ initialValue,
3889
+ onSubmit,
3890
+ onCancel
3891
+ }) {
3892
+ const initialIndex = Math.max(
3893
+ 0,
3894
+ items.findIndex((i) => i.value === initialValue && !i.disabled)
3895
+ );
3896
+ const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
3897
+ useInput3((_input, key) => {
3898
+ if (key.upArrow) {
3899
+ setIndex((i) => findNextEnabled(items, i, -1));
3900
+ } else if (key.downArrow) {
3901
+ setIndex((i) => findNextEnabled(items, i, 1));
3902
+ } else if (key.return) {
3903
+ const chosen = items[index];
3904
+ if (chosen && !chosen.disabled) onSubmit(chosen.value);
3905
+ } else if (key.escape && onCancel) {
3906
+ onCancel();
3907
+ }
3908
+ });
3909
+ return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React14.createElement(
3910
+ SelectRow,
3911
+ {
3912
+ key: item.value,
3913
+ item,
3914
+ active: i === index,
3915
+ marker: i === index ? "\u25B8" : " "
3916
+ }
3917
+ )));
3918
+ }
3919
+ function MultiSelect({
3920
+ items,
3921
+ initialSelected = [],
3922
+ onSubmit,
3923
+ onCancel,
3924
+ footer
3925
+ }) {
3926
+ const [index, setIndex] = useState7(() => {
3927
+ const first = items.findIndex((i) => !i.disabled);
3928
+ return first === -1 ? 0 : first;
3929
+ });
3930
+ const [selected, setSelected] = useState7(new Set(initialSelected));
3931
+ useInput3((input, key) => {
3932
+ if (key.upArrow) {
3933
+ setIndex((i) => findNextEnabled(items, i, -1));
3934
+ } else if (key.downArrow) {
3935
+ setIndex((i) => findNextEnabled(items, i, 1));
3936
+ } else if (input === " ") {
3937
+ const item = items[index];
3938
+ if (!item || item.disabled) return;
3939
+ setSelected((prev) => {
3940
+ const next = new Set(prev);
3941
+ if (next.has(item.value)) next.delete(item.value);
3942
+ else next.add(item.value);
3943
+ return next;
3944
+ });
3945
+ } else if (key.return) {
3946
+ const ordered = items.filter((i) => selected.has(i.value)).map((i) => i.value);
3947
+ onSubmit(ordered);
3948
+ } else if (key.escape && onCancel) {
3949
+ onCancel();
3950
+ }
3951
+ });
3952
+ return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => {
3953
+ const checked = selected.has(item.value);
3954
+ const marker = checked ? "[x]" : "[ ]";
3955
+ return /* @__PURE__ */ React14.createElement(
3956
+ SelectRow,
3957
+ {
3958
+ key: item.value,
3959
+ item,
3960
+ active: i === index,
3961
+ marker: `${i === index ? "\u25B8" : " "} ${marker}`
3962
+ }
3963
+ );
3964
+ }), footer ? /* @__PURE__ */ React14.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, footer)) : null);
3965
+ }
3966
+ function SelectRow({
3967
+ item,
3968
+ active,
3969
+ marker
3970
+ }) {
3971
+ const color = item.disabled ? "gray" : active ? "cyan" : void 0;
3972
+ return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React14.createElement(Box11, null, /* @__PURE__ */ React14.createElement(Text11, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React14.createElement(Box11, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, item.hint)) : null);
3973
+ }
3974
+ function findNextEnabled(items, from, step) {
3975
+ if (items.length === 0) return 0;
3976
+ let i = from;
3977
+ for (let tries = 0; tries < items.length; tries++) {
3978
+ i = (i + step + items.length) % items.length;
3979
+ if (!items[i]?.disabled) return i;
3980
+ }
3981
+ return from;
3982
+ }
3983
+
3984
+ // src/cli/ui/presets.ts
3985
+ var PRESETS = {
3986
+ fast: { model: "deepseek-chat", harvest: false, branch: 1 },
3987
+ smart: { model: "deepseek-reasoner", harvest: true, branch: 1 },
3988
+ max: { model: "deepseek-reasoner", harvest: true, branch: 3 }
3989
+ };
3990
+ var PRESET_DESCRIPTIONS = {
3991
+ fast: {
3992
+ headline: "deepseek-chat, no reasoning harvest, no branching",
3993
+ cost: "~1\xA2 per 100 turns \xB7 default"
3994
+ },
3995
+ smart: {
3996
+ headline: "deepseek-reasoner + Pillar 2 harvest",
3997
+ cost: "~10\xD7 cost vs fast \xB7 slower \xB7 better on multi-step tasks"
3998
+ },
3999
+ max: {
4000
+ headline: "reasoner + harvest + self-consistency (3 branches)",
4001
+ cost: "~30\xD7 cost vs fast \xB7 slowest \xB7 for hard single-shots"
4002
+ }
4003
+ };
4004
+
4005
+ // src/cli/ui/Wizard.tsx
4006
+ var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
4007
+ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
4008
+ const { exit } = useApp5();
4009
+ const [step, setStep] = useState8(existingApiKey ? "preset" : "apiKey");
4010
+ const [data, setData] = useState8({
4011
+ apiKey: existingApiKey ?? "",
4012
+ preset: initial?.preset ?? "fast",
4013
+ selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
4014
+ catalogArgs: {}
4015
+ });
4016
+ const [error, setError] = useState8(null);
4017
+ useInput4((_input, key) => {
4018
+ if (key.escape && step !== "saved" && onCancel) onCancel();
4019
+ });
4020
+ if (step === "apiKey") {
4021
+ return /* @__PURE__ */ React15.createElement(
4022
+ ApiKeyStep,
4023
+ {
4024
+ onSubmit: (key) => {
4025
+ setData((d) => ({ ...d, apiKey: key }));
4026
+ setError(null);
4027
+ setStep("preset");
4028
+ },
4029
+ error,
4030
+ onError: setError
4031
+ }
4032
+ );
4033
+ }
4034
+ if (step === "preset") {
4035
+ return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React15.createElement(
4036
+ SingleSelect,
4037
+ {
4038
+ items: presetItems(),
4039
+ initialValue: data.preset,
4040
+ onSubmit: (preset) => {
4041
+ setData((d) => ({ ...d, preset }));
4042
+ setStep("mcp");
4043
+ }
4044
+ }
4045
+ ), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
4046
+ }
4047
+ if (step === "mcp") {
4048
+ return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React15.createElement(
4049
+ MultiSelect,
4050
+ {
4051
+ items: mcpItems(),
4052
+ initialSelected: data.selectedCatalog,
4053
+ onSubmit: (selected) => {
4054
+ setData((d) => ({ ...d, selectedCatalog: selected }));
4055
+ const needsArgs = selected.some((name) => CATALOG_BY_NAME.get(name)?.userArgs);
4056
+ setStep(needsArgs ? "mcpArgs" : "review");
4057
+ },
4058
+ footer: "\u2191/\u2193 move \xB7 space toggle \xB7 enter confirm \xB7 esc cancel \xB7 leave empty to skip"
4059
+ }
4060
+ ));
4061
+ }
4062
+ if (step === "mcpArgs") {
4063
+ const pending = data.selectedCatalog.filter((name) => {
4064
+ const entry2 = CATALOG_BY_NAME.get(name);
4065
+ return entry2?.userArgs && !data.catalogArgs[name];
4066
+ });
4067
+ if (pending.length === 0) {
4068
+ setStep("review");
4069
+ return null;
4070
+ }
4071
+ const currentName = pending[0];
4072
+ const entry = CATALOG_BY_NAME.get(currentName);
4073
+ return /* @__PURE__ */ React15.createElement(
4074
+ McpArgsStep,
4075
+ {
4076
+ entry,
4077
+ error,
4078
+ onSubmit: (value) => {
4079
+ setData((d) => ({
4080
+ ...d,
4081
+ catalogArgs: { ...d.catalogArgs, [currentName]: value }
4082
+ }));
4083
+ setError(null);
4084
+ },
4085
+ onError: setError
4086
+ }
4087
+ );
4088
+ }
4089
+ if (step === "review") {
4090
+ const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
4091
+ return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React15.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React15.createElement(
4092
+ SummaryLine,
4093
+ {
4094
+ label: "MCP",
4095
+ value: specs.length === 0 ? "(none)" : `${specs.length} server(s)`
4096
+ }
4097
+ ), specs.map((spec, i) => (
4098
+ // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
4099
+ /* @__PURE__ */ React15.createElement(Box12, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\xB7 ", spec))
4100
+ )), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null, /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React15.createElement(
4101
+ ReviewConfirm,
4102
+ {
4103
+ onConfirm: () => {
4104
+ try {
4105
+ const specsNow = data.selectedCatalog.map(
4106
+ (name) => buildSpec(name, data.catalogArgs)
4107
+ );
4108
+ const prev = readConfig();
4109
+ const next = {
4110
+ ...prev,
4111
+ apiKey: data.apiKey,
4112
+ preset: data.preset,
4113
+ mcp: specsNow,
4114
+ setupCompleted: true
4115
+ };
4116
+ writeConfig(next);
4117
+ setStep("saved");
4118
+ onComplete(next);
4119
+ } catch (e) {
4120
+ setError(`Could not save config: ${e.message}`);
4121
+ }
4122
+ }
4123
+ }
4124
+ ));
4125
+ }
4126
+ return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React15.createElement(ExitOnEnter, { onExit: exit }));
4127
+ }
4128
+ function ApiKeyStep({
4129
+ onSubmit,
4130
+ error,
4131
+ onError
4132
+ }) {
4133
+ const [value, setValue] = useState8("");
4134
+ return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React15.createElement(
4135
+ TextInput3,
4136
+ {
4137
+ value,
4138
+ onChange: setValue,
4139
+ onSubmit: (raw) => {
4140
+ const trimmed = raw.trim();
4141
+ if (!isPlausibleKey(trimmed)) {
4142
+ onError("Doesn't look like a DeepSeek key. They start with 'sk-' and are 30+ chars.");
4143
+ setValue("");
4144
+ return;
4145
+ }
4146
+ onSubmit(trimmed);
4147
+ },
4148
+ mask: "\u2022",
4149
+ placeholder: "sk-..."
4150
+ }
4151
+ )), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : value ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "preview: ", redactKey(value))) : null);
4152
+ }
4153
+ function McpArgsStep({
4154
+ entry,
4155
+ error,
4156
+ onSubmit,
4157
+ onError
4158
+ }) {
4159
+ const [value, setValue] = useState8("");
4160
+ return /* @__PURE__ */ React15.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(Text12, null, entry.summary), entry.note ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Required parameter: "), /* @__PURE__ */ React15.createElement(Text12, { bold: true }, entry.userArgs)), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React15.createElement(
4161
+ TextInput3,
4162
+ {
4163
+ value,
4164
+ onChange: setValue,
4165
+ onSubmit: (raw) => {
4166
+ const trimmed = raw.trim();
4167
+ if (!trimmed) {
4168
+ onError(`${entry.name} needs a value \u2014 got an empty string.`);
4169
+ return;
4170
+ }
4171
+ onSubmit(trimmed);
4172
+ setValue("");
4173
+ },
4174
+ placeholder: placeholderFor(entry)
4175
+ }
4176
+ )), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null));
4177
+ }
4178
+ function ReviewConfirm({ onConfirm }) {
4179
+ useInput4((_i, key) => {
4180
+ if (key.return) onConfirm();
4181
+ });
4182
+ return null;
4183
+ }
4184
+ function ExitOnEnter({ onExit }) {
4185
+ useInput4((_i, key) => {
4186
+ if (key.return) onExit();
4187
+ });
4188
+ return null;
4189
+ }
4190
+ function StepFrame({
4191
+ title,
4192
+ step,
4193
+ total,
4194
+ children
4195
+ }) {
4196
+ return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1, flexDirection: "column" }, children));
4197
+ }
4198
+ function SummaryLine({ label, value }) {
4199
+ return /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, null, label.padEnd(12)), /* @__PURE__ */ React15.createElement(Text12, { bold: true }, value));
4200
+ }
4201
+ function presetItems() {
4202
+ return ["fast", "smart", "max"].map((name) => ({
4203
+ value: name,
4204
+ label: `${name} \u2014 ${PRESET_DESCRIPTIONS[name].headline}`,
4205
+ hint: PRESET_DESCRIPTIONS[name].cost
4206
+ }));
4207
+ }
4208
+ function mcpItems() {
4209
+ return MCP_CATALOG.map((entry) => {
4210
+ const hintParts = [entry.summary];
4211
+ if (entry.userArgs) hintParts.push(`(you'll provide ${entry.userArgs})`);
4212
+ if (entry.note) hintParts.push(entry.note);
4213
+ return {
4214
+ value: entry.name,
4215
+ label: entry.name,
4216
+ hint: hintParts.join(" \xB7 ")
4217
+ };
4218
+ });
4219
+ }
4220
+ function placeholderFor(entry) {
4221
+ if (entry.name === "filesystem") return "e.g. /tmp/reasonix-sandbox";
4222
+ if (entry.name === "sqlite") return "e.g. ./notes.sqlite";
4223
+ return entry.userArgs ?? "";
4224
+ }
4225
+ function deriveInitialCatalog(existingSpecs) {
4226
+ const packageToName = new Map(MCP_CATALOG.map((e) => [e.package, e.name]));
4227
+ const out = [];
4228
+ for (const spec of existingSpecs) {
4229
+ for (const [pkg, name] of packageToName) {
4230
+ if (spec.includes(pkg)) {
4231
+ out.push(name);
4232
+ break;
4233
+ }
4234
+ }
4235
+ }
4236
+ return out;
4237
+ }
4238
+ function buildSpec(name, argsByName) {
4239
+ const entry = CATALOG_BY_NAME.get(name);
4240
+ if (!entry) return name;
4241
+ const userArg = entry.userArgs ? argsByName[name] : void 0;
4242
+ const tail = userArg ? ` ${quoteIfNeeded(userArg)}` : "";
4243
+ return `${entry.name}=npx -y ${entry.package}${tail}`;
4244
+ }
4245
+ function quoteIfNeeded(s) {
4246
+ return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
4247
+ }
4248
+
4249
+ // src/cli/commands/setup.tsx
4250
+ async function setupCommand(_opts = {}) {
4251
+ loadDotenv();
4252
+ const existingKey = loadApiKey();
4253
+ const existing = readConfig();
4254
+ const { waitUntilExit, unmount } = render4(
4255
+ /* @__PURE__ */ React16.createElement(
4256
+ Wizard,
4257
+ {
4258
+ existingApiKey: existingKey,
4259
+ initial: { preset: existing.preset, mcp: existing.mcp },
4260
+ onComplete: () => {
4261
+ },
4262
+ onCancel: () => {
4263
+ unmount();
4264
+ }
4265
+ }
4266
+ ),
4267
+ { exitOnCtrlC: true }
4268
+ );
4269
+ await waitUntilExit();
4270
+ }
4271
+
3640
4272
  // src/cli/commands/stats.ts
3641
4273
  import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
3642
4274
  function statsCommand(opts) {
@@ -3668,72 +4300,131 @@ function versionCommand() {
3668
4300
  console.log(`reasonix ${VERSION}`);
3669
4301
  }
3670
4302
 
4303
+ // src/cli/resolve.ts
4304
+ function resolveDefaults(flags) {
4305
+ const cfg = flags.noConfig ? {} : readConfig();
4306
+ const preset = pickPreset(flags.preset, cfg.preset);
4307
+ const presetSettings = PRESETS[preset];
4308
+ const model = flags.model ?? presetSettings.model;
4309
+ const harvest2 = flags.harvest === true ? true : presetSettings.harvest;
4310
+ const branchFromFlag = normalizeBranch(flags.branch);
4311
+ const branch = branchFromFlag ?? (presetSettings.branch > 1 ? presetSettings.branch : void 0);
4312
+ const mcp2 = flags.mcp && flags.mcp.length > 0 ? flags.mcp : cfg.mcp ?? [];
4313
+ const session = resolveSession(flags.session, cfg.session);
4314
+ return { model, harvest: harvest2, branch, mcp: mcp2, session };
4315
+ }
4316
+ function pickPreset(flagPreset, configPreset) {
4317
+ if (flagPreset && isPresetName(flagPreset)) return flagPreset;
4318
+ if (configPreset) return configPreset;
4319
+ return "fast";
4320
+ }
4321
+ function isPresetName(s) {
4322
+ return s === "fast" || s === "smart" || s === "max";
4323
+ }
4324
+ function normalizeBranch(raw) {
4325
+ if (raw === void 0) return void 0;
4326
+ if (!Number.isFinite(raw) || raw <= 1) return void 0;
4327
+ return Math.min(raw, 8);
4328
+ }
4329
+ function resolveSession(flag, configSession) {
4330
+ if (flag === false) return void 0;
4331
+ if (typeof flag === "string" && flag.length > 0) return flag;
4332
+ if (configSession === null) return void 0;
4333
+ if (typeof configSession === "string" && configSession.length > 0) return configSession;
4334
+ return "default";
4335
+ }
4336
+
3671
4337
  // src/cli/index.ts
3672
4338
  var DEFAULT_SYSTEM = "You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.";
3673
4339
  var program = new Command();
3674
4340
  program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
3675
- program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
4341
+ program.action(async () => {
4342
+ const cfg = readConfig();
4343
+ if (!cfg.setupCompleted) {
4344
+ await setupCommand({});
4345
+ return;
4346
+ }
4347
+ const defaults = resolveDefaults({});
4348
+ await chatCommand({
4349
+ model: defaults.model,
4350
+ system: DEFAULT_SYSTEM,
4351
+ harvest: defaults.harvest,
4352
+ branch: defaults.branch,
4353
+ session: defaults.session,
4354
+ mcp: defaults.mcp
4355
+ });
4356
+ });
4357
+ program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
4358
+ await setupCommand({});
4359
+ });
4360
+ program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
4361
+ "--preset <name>",
4362
+ "Bundle of model + harvest + branch. One of: fast, smart, max. Overrides config.preset."
4363
+ ).option(
3676
4364
  "--harvest",
3677
- "Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
4365
+ "Extract typed plan state from R1 reasoning (Pillar 2). Overrides preset's harvest setting."
3678
4366
  ).option(
3679
4367
  "--branch <n>",
3680
4368
  "Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
3681
4369
  (v) => Number.parseInt(v, 10)
3682
- ).option(
3683
- "--session <name>",
3684
- "Use a named session (default: 'default'). Resume the same session next time."
3685
- ).option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
4370
+ ).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
3686
4371
  "--mcp <spec>",
3687
- 'MCP server spec; repeatable. Forms: "name=cmd args..." (namespaced, tools get `name_` prefix) or "cmd args..." (anonymous). Example: --mcp "fs=npx -y @scope/fs /tmp" --mcp "gh=npx -y @scope/gh"',
4372
+ 'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE transport). Overrides config.mcp when provided.',
3688
4373
  (value, previous = []) => [...previous, value],
3689
4374
  []
3690
4375
  ).option(
3691
4376
  "--mcp-prefix <str>",
3692
4377
  "Global prefix applied to every MCP tool (only honored when no per-spec name is set; avoids collisions with a single anonymous server)"
3693
- ).action(async (opts) => {
3694
- let session;
3695
- if (opts.session === false) {
3696
- session = void 0;
3697
- } else if (typeof opts.session === "string" && opts.session.length > 0) {
3698
- session = opts.session;
3699
- } else {
3700
- session = "default";
3701
- }
3702
- await chatCommand({
4378
+ ).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (opts) => {
4379
+ const defaults = resolveDefaults({
3703
4380
  model: opts.model,
4381
+ harvest: opts.harvest,
4382
+ branch: opts.branch,
4383
+ mcp: opts.mcp,
4384
+ session: opts.session,
4385
+ preset: opts.preset,
4386
+ noConfig: opts.config === false
4387
+ });
4388
+ await chatCommand({
4389
+ model: defaults.model,
3704
4390
  system: opts.system,
3705
4391
  transcript: opts.transcript,
3706
- harvest: !!opts.harvest,
3707
- branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
3708
- session,
3709
- mcp: opts.mcp,
4392
+ harvest: defaults.harvest,
4393
+ branch: defaults.branch,
4394
+ session: defaults.session,
4395
+ mcp: defaults.mcp,
3710
4396
  mcpPrefix: opts.mcpPrefix
3711
4397
  });
3712
4398
  });
3713
- program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).option(
3714
- "--harvest",
3715
- "Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
3716
- ).option(
4399
+ program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).option("--preset <name>", "Bundle of model + harvest + branch: fast | smart | max").option("--harvest", "Extract typed plan state from R1 reasoning (Pillar 2)").option(
3717
4400
  "--branch <n>",
3718
4401
  "Self-consistency: run N parallel samples per turn and pick the most confident",
3719
4402
  (v) => Number.parseInt(v, 10)
3720
4403
  ).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
3721
4404
  "--mcp <spec>",
3722
- 'MCP server spec; repeatable. "name=cmd args..." or "cmd args...".',
4405
+ 'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE).',
3723
4406
  (value, previous = []) => [...previous, value],
3724
4407
  []
3725
4408
  ).option(
3726
4409
  "--mcp-prefix <str>",
3727
4410
  "Global prefix (only honored when no per-spec name is set; for a single anonymous server)"
3728
- ).action(async (task, opts) => {
4411
+ ).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (task, opts) => {
4412
+ const defaults = resolveDefaults({
4413
+ model: opts.model,
4414
+ harvest: opts.harvest,
4415
+ branch: opts.branch,
4416
+ mcp: opts.mcp,
4417
+ preset: opts.preset,
4418
+ noConfig: opts.config === false
4419
+ });
3729
4420
  await runCommand({
3730
4421
  task,
3731
- model: opts.model,
4422
+ model: defaults.model,
3732
4423
  system: opts.system,
3733
- harvest: !!opts.harvest,
3734
- branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
4424
+ harvest: defaults.harvest,
4425
+ branch: defaults.branch,
3735
4426
  transcript: opts.transcript,
3736
- mcp: opts.mcp,
4427
+ mcp: defaults.mcp,
3737
4428
  mcpPrefix: opts.mcpPrefix
3738
4429
  });
3739
4430
  });