reasonix 0.3.0-alpha.4 → 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;
@@ -925,87 +1095,12 @@ var SessionStats = class {
925
1095
  claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
926
1096
  savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
927
1097
  cacheHitRatio: round(this.aggregateCacheHitRatio, 4)
928
- };
929
- }
930
- };
931
- function round(n, digits) {
932
- const f = 10 ** digits;
933
- return Math.round(n * f) / f;
934
- }
935
-
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;
1098
+ };
1007
1099
  }
1008
- return false;
1100
+ };
1101
+ function round(n, digits) {
1102
+ const f = 10 ** digits;
1103
+ return Math.round(n * f) / f;
1009
1104
  }
1010
1105
 
1011
1106
  // src/loop.ts
@@ -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) {
@@ -2242,45 +2369,6 @@ var SseTransport = class {
2242
2369
  }
2243
2370
  };
2244
2371
 
2245
- // src/mcp/registry.ts
2246
- async function bridgeMcpTools(client, opts = {}) {
2247
- const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
2248
- const prefix = opts.namePrefix ?? "";
2249
- const result = { registry, registeredNames: [], skipped: [] };
2250
- const listed = await client.listTools();
2251
- for (const mcpTool of listed.tools) {
2252
- if (!mcpTool.name) {
2253
- result.skipped.push({ name: "?", reason: "empty tool name" });
2254
- continue;
2255
- }
2256
- const registeredName = `${prefix}${mcpTool.name}`;
2257
- registry.register({
2258
- name: registeredName,
2259
- description: mcpTool.description ?? "",
2260
- parameters: mcpTool.inputSchema,
2261
- fn: async (args) => {
2262
- const toolResult = await client.callTool(mcpTool.name, args);
2263
- return flattenMcpResult(toolResult);
2264
- }
2265
- });
2266
- result.registeredNames.push(registeredName);
2267
- }
2268
- return result;
2269
- }
2270
- function flattenMcpResult(result) {
2271
- const parts = result.content.map(blockToString);
2272
- const joined = parts.join("\n").trim();
2273
- if (result.isError) {
2274
- return `ERROR: ${joined || "(no error message from server)"}`;
2275
- }
2276
- return joined;
2277
- }
2278
- function blockToString(block) {
2279
- if (block.type === "text") return block.text;
2280
- if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
2281
- return `[unknown block: ${JSON.stringify(block)}]`;
2282
- }
2283
-
2284
2372
  // src/mcp/shell-split.ts
2285
2373
  function shellSplit(input) {
2286
2374
  const tokens = [];
@@ -2355,51 +2443,8 @@ function parseMcpSpec(input) {
2355
2443
  return { transport: "stdio", name, command, args };
2356
2444
  }
2357
2445
 
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)}`;
2399
- }
2400
-
2401
2446
  // src/index.ts
2402
- var VERSION = "0.3.0-alpha.4";
2447
+ var VERSION = "0.3.0-alpha.6";
2403
2448
 
2404
2449
  // src/cli/commands/chat.tsx
2405
2450
  import { render } from "ink";
@@ -2738,7 +2783,7 @@ function parseSlash(text) {
2738
2783
  if (!cmd) return null;
2739
2784
  return { cmd, args: parts.slice(1) };
2740
2785
  }
2741
- function handleSlash(cmd, args, loop) {
2786
+ function handleSlash(cmd, args, loop, ctx = {}) {
2742
2787
  switch (cmd) {
2743
2788
  case "exit":
2744
2789
  case "quit":
@@ -2756,6 +2801,8 @@ function handleSlash(cmd, args, loop) {
2756
2801
  " /model <id> deepseek-chat or deepseek-reasoner",
2757
2802
  " /harvest [on|off] Pillar 2: structured plan-state extraction",
2758
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`",
2759
2806
  " /sessions list saved sessions (current is marked with \u25B8)",
2760
2807
  " /forget delete the current session from disk",
2761
2808
  " /clear clear displayed history (log + session kept)",
@@ -2771,6 +2818,32 @@ function handleSlash(cmd, args, loop) {
2771
2818
  " reasonix chat --no-session disable persistence for this run"
2772
2819
  ].join("\n")
2773
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
+ };
2774
2847
  case "sessions": {
2775
2848
  const items = listSessions();
2776
2849
  if (items.length === 0) {
@@ -2860,7 +2933,16 @@ function handleSlash(cmd, args, loop) {
2860
2933
 
2861
2934
  // src/cli/ui/App.tsx
2862
2935
  var FLUSH_INTERVAL_MS = 60;
2863
- 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
+ }) {
2864
2946
  const { exit } = useApp();
2865
2947
  const [historical, setHistorical] = useState2([]);
2866
2948
  const [streaming, setStreaming] = useState2(null);
@@ -2948,7 +3030,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
2948
3030
  setInput("");
2949
3031
  const slash = parseSlash(text);
2950
3032
  if (slash) {
2951
- const result = handleSlash(slash.cmd, slash.args, loop);
3033
+ const result = handleSlash(slash.cmd, slash.args, loop, { mcpSpecs });
2952
3034
  if (result.exit) {
2953
3035
  transcriptRef.current?.end();
2954
3036
  exit();
@@ -3059,7 +3141,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
3059
3141
  setBusy(false);
3060
3142
  }
3061
3143
  },
3062
- [busy, exit, loop, writeTranscript]
3144
+ [busy, exit, loop, mcpSpecs, writeTranscript]
3063
3145
  );
3064
3146
  return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
3065
3147
  StatsPanel,
@@ -3073,7 +3155,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
3073
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));
3074
3156
  }
3075
3157
  function CommandStrip() {
3076
- 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"));
3077
3159
  }
3078
3160
  function describeRepair(repair) {
3079
3161
  const parts = [];
@@ -3123,7 +3205,7 @@ function Setup({ onReady }) {
3123
3205
  }
3124
3206
 
3125
3207
  // src/cli/commands/chat.tsx
3126
- function Root({ initialKey, tools, ...appProps }) {
3208
+ function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
3127
3209
  const [key, setKey] = useState4(initialKey);
3128
3210
  if (!key) {
3129
3211
  return /* @__PURE__ */ React8.createElement(
@@ -3146,22 +3228,25 @@ function Root({ initialKey, tools, ...appProps }) {
3146
3228
  harvest: appProps.harvest,
3147
3229
  branch: appProps.branch,
3148
3230
  session: appProps.session,
3149
- tools
3231
+ tools,
3232
+ mcpSpecs
3150
3233
  }
3151
3234
  );
3152
3235
  }
3153
3236
  async function chatCommand(opts) {
3154
3237
  loadDotenv();
3155
3238
  const initialKey = loadApiKey();
3156
- const mcpSpecs = opts.mcp ?? [];
3239
+ const requestedSpecs = opts.mcp ?? [];
3157
3240
  const clients = [];
3241
+ const successfulSpecs = [];
3242
+ const failedSpecs = [];
3158
3243
  let tools;
3159
- if (mcpSpecs.length > 0) {
3244
+ if (requestedSpecs.length > 0) {
3160
3245
  tools = new ToolRegistry();
3161
- for (const raw of mcpSpecs) {
3246
+ for (const raw of requestedSpecs) {
3162
3247
  try {
3163
3248
  const spec = parseMcpSpec(raw);
3164
- const prefix = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3249
+ const prefix = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3165
3250
  const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
3166
3251
  const mcp2 = new McpClient({ transport });
3167
3252
  await mcp2.initialize();
@@ -3173,17 +3258,26 @@ async function chatCommand(opts) {
3173
3258
  `
3174
3259
  );
3175
3260
  clients.push(mcp2);
3261
+ successfulSpecs.push(raw);
3176
3262
  } catch (err) {
3177
- process.stderr.write(`MCP setup failed for "${raw}": ${err.message}
3178
- `);
3179
- for (const c of clients) await c.close();
3180
- 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
+ );
3181
3270
  }
3182
3271
  }
3272
+ if (successfulSpecs.length === 0) {
3273
+ tools = void 0;
3274
+ }
3183
3275
  }
3184
- const { waitUntilExit } = render(/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, ...opts }), {
3185
- exitOnCtrlC: true
3186
- });
3276
+ const mcpSpecs = successfulSpecs;
3277
+ const { waitUntilExit } = render(
3278
+ /* @__PURE__ */ React8.createElement(Root, { initialKey, tools, mcpSpecs, ...opts }),
3279
+ { exitOnCtrlC: true }
3280
+ );
3187
3281
  try {
3188
3282
  await waitUntilExit();
3189
3283
  } finally {
@@ -3362,11 +3456,6 @@ var MCP_CATALOG = [
3362
3456
  userArgs: "<dir>",
3363
3457
  note: "the directory is a hard sandbox \u2014 the server refuses access outside it"
3364
3458
  },
3365
- {
3366
- name: "fetch",
3367
- summary: "fetch URLs (markdown-friendly extraction, not a full browser)",
3368
- package: "@modelcontextprotocol/server-fetch"
3369
- },
3370
3459
  {
3371
3460
  name: "memory",
3372
3461
  summary: "persistent key-value memory across sessions",
@@ -3378,12 +3467,6 @@ var MCP_CATALOG = [
3378
3467
  package: "@modelcontextprotocol/server-github",
3379
3468
  note: "set GITHUB_PERSONAL_ACCESS_TOKEN in your env before spawning"
3380
3469
  },
3381
- {
3382
- name: "sqlite",
3383
- summary: "read/write a sqlite database file",
3384
- package: "@modelcontextprotocol/server-sqlite",
3385
- userArgs: "<db.sqlite>"
3386
- },
3387
3470
  {
3388
3471
  name: "puppeteer",
3389
3472
  summary: "browser automation \u2014 take screenshots, click, type",
@@ -3619,15 +3702,16 @@ async function runCommand(opts) {
3619
3702
  loadDotenv();
3620
3703
  const apiKey = await ensureApiKey();
3621
3704
  process.env.DEEPSEEK_API_KEY = apiKey;
3622
- const mcpSpecs = opts.mcp ?? [];
3705
+ const requestedSpecs = opts.mcp ?? [];
3623
3706
  const clients = [];
3624
3707
  let tools;
3625
- if (mcpSpecs.length > 0) {
3708
+ let successCount = 0;
3709
+ if (requestedSpecs.length > 0) {
3626
3710
  tools = new ToolRegistry();
3627
- for (const raw of mcpSpecs) {
3711
+ for (const raw of requestedSpecs) {
3628
3712
  try {
3629
3713
  const spec = parseMcpSpec(raw);
3630
- const prefix2 = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3714
+ const prefix2 = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3631
3715
  const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
3632
3716
  const mcp2 = new McpClient({ transport });
3633
3717
  await mcp2.initialize();
@@ -3638,13 +3722,16 @@ async function runCommand(opts) {
3638
3722
  `
3639
3723
  );
3640
3724
  clients.push(mcp2);
3725
+ successCount++;
3641
3726
  } catch (err) {
3642
- process.stderr.write(`MCP setup failed for "${raw}": ${err.message}
3643
- `);
3644
- for (const c of clients) await c.close();
3645
- 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
+ );
3646
3732
  }
3647
3733
  }
3734
+ if (successCount === 0) tools = void 0;
3648
3735
  }
3649
3736
  const client = new DeepSeekClient();
3650
3737
  const prefix = new ImmutablePrefix({
@@ -3784,6 +3871,404 @@ function truncate4(s, max) {
3784
3871
  return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
3785
3872
  }
3786
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
+
3787
4272
  // src/cli/commands/stats.ts
3788
4273
  import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
3789
4274
  function statsCommand(opts) {
@@ -3815,72 +4300,131 @@ function versionCommand() {
3815
4300
  console.log(`reasonix ${VERSION}`);
3816
4301
  }
3817
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
+
3818
4337
  // src/cli/index.ts
3819
4338
  var DEFAULT_SYSTEM = "You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.";
3820
4339
  var program = new Command();
3821
4340
  program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
3822
- 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(
3823
4364
  "--harvest",
3824
- "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."
3825
4366
  ).option(
3826
4367
  "--branch <n>",
3827
4368
  "Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
3828
4369
  (v) => Number.parseInt(v, 10)
3829
- ).option(
3830
- "--session <name>",
3831
- "Use a named session (default: 'default'). Resume the same session next time."
3832
- ).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(
3833
4371
  "--mcp <spec>",
3834
- '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.',
3835
4373
  (value, previous = []) => [...previous, value],
3836
4374
  []
3837
4375
  ).option(
3838
4376
  "--mcp-prefix <str>",
3839
4377
  "Global prefix applied to every MCP tool (only honored when no per-spec name is set; avoids collisions with a single anonymous server)"
3840
- ).action(async (opts) => {
3841
- let session;
3842
- if (opts.session === false) {
3843
- session = void 0;
3844
- } else if (typeof opts.session === "string" && opts.session.length > 0) {
3845
- session = opts.session;
3846
- } else {
3847
- session = "default";
3848
- }
3849
- await chatCommand({
4378
+ ).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (opts) => {
4379
+ const defaults = resolveDefaults({
3850
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,
3851
4390
  system: opts.system,
3852
4391
  transcript: opts.transcript,
3853
- harvest: !!opts.harvest,
3854
- branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
3855
- session,
3856
- mcp: opts.mcp,
4392
+ harvest: defaults.harvest,
4393
+ branch: defaults.branch,
4394
+ session: defaults.session,
4395
+ mcp: defaults.mcp,
3857
4396
  mcpPrefix: opts.mcpPrefix
3858
4397
  });
3859
4398
  });
3860
- 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(
3861
- "--harvest",
3862
- "Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
3863
- ).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(
3864
4400
  "--branch <n>",
3865
4401
  "Self-consistency: run N parallel samples per turn and pick the most confident",
3866
4402
  (v) => Number.parseInt(v, 10)
3867
4403
  ).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
3868
4404
  "--mcp <spec>",
3869
- '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).',
3870
4406
  (value, previous = []) => [...previous, value],
3871
4407
  []
3872
4408
  ).option(
3873
4409
  "--mcp-prefix <str>",
3874
4410
  "Global prefix (only honored when no per-spec name is set; for a single anonymous server)"
3875
- ).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
+ });
3876
4420
  await runCommand({
3877
4421
  task,
3878
- model: opts.model,
4422
+ model: defaults.model,
3879
4423
  system: opts.system,
3880
- harvest: !!opts.harvest,
3881
- branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
4424
+ harvest: defaults.harvest,
4425
+ branch: defaults.branch,
3882
4426
  transcript: opts.transcript,
3883
- mcp: opts.mcp,
4427
+ mcp: defaults.mcp,
3884
4428
  mcpPrefix: opts.mcpPrefix
3885
4429
  });
3886
4430
  });