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 +862 -318
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +83 -3
- package/dist/index.js +235 -186
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
970
|
+
return join2(homedir2(), ".reasonix", "sessions");
|
|
801
971
|
}
|
|
802
972
|
function sessionPath(name) {
|
|
803
|
-
return
|
|
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 =
|
|
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
|
-
|
|
1001
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
832
1002
|
appendFileSync(path, `${JSON.stringify(message)}
|
|
833
1003
|
`, "utf8");
|
|
834
1004
|
try {
|
|
835
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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({
|
|
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 /
|
|
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
|
|
3239
|
+
const requestedSpecs = opts.mcp ?? [];
|
|
3157
3240
|
const clients = [];
|
|
3241
|
+
const successfulSpecs = [];
|
|
3242
|
+
const failedSpecs = [];
|
|
3158
3243
|
let tools;
|
|
3159
|
-
if (
|
|
3244
|
+
if (requestedSpecs.length > 0) {
|
|
3160
3245
|
tools = new ToolRegistry();
|
|
3161
|
-
for (const raw of
|
|
3246
|
+
for (const raw of requestedSpecs) {
|
|
3162
3247
|
try {
|
|
3163
3248
|
const spec = parseMcpSpec(raw);
|
|
3164
|
-
const prefix = spec.name ? `${spec.name}_` :
|
|
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
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
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
|
|
3185
|
-
|
|
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
|
|
3705
|
+
const requestedSpecs = opts.mcp ?? [];
|
|
3623
3706
|
const clients = [];
|
|
3624
3707
|
let tools;
|
|
3625
|
-
|
|
3708
|
+
let successCount = 0;
|
|
3709
|
+
if (requestedSpecs.length > 0) {
|
|
3626
3710
|
tools = new ToolRegistry();
|
|
3627
|
-
for (const raw of
|
|
3711
|
+
for (const raw of requestedSpecs) {
|
|
3628
3712
|
try {
|
|
3629
3713
|
const spec = parseMcpSpec(raw);
|
|
3630
|
-
const prefix2 = spec.name ? `${spec.name}_` :
|
|
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(
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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:
|
|
3854
|
-
branch:
|
|
3855
|
-
session,
|
|
3856
|
-
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
|
|
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..."
|
|
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:
|
|
4422
|
+
model: defaults.model,
|
|
3879
4423
|
system: opts.system,
|
|
3880
|
-
harvest:
|
|
3881
|
-
branch:
|
|
4424
|
+
harvest: defaults.harvest,
|
|
4425
|
+
branch: defaults.branch,
|
|
3882
4426
|
transcript: opts.transcript,
|
|
3883
|
-
mcp:
|
|
4427
|
+
mcp: defaults.mcp,
|
|
3884
4428
|
mcpPrefix: opts.mcpPrefix
|
|
3885
4429
|
});
|
|
3886
4430
|
});
|