reasonix 0.3.0-alpha.3 → 0.3.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli/index.js +1011 -320
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +177 -3
- package/dist/index.js +442 -178
- 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;
|
|
@@ -933,81 +1103,6 @@ function round(n, digits) {
|
|
|
933
1103
|
return Math.round(n * f) / f;
|
|
934
1104
|
}
|
|
935
1105
|
|
|
936
|
-
// src/tools.ts
|
|
937
|
-
var ToolRegistry = class {
|
|
938
|
-
_tools = /* @__PURE__ */ new Map();
|
|
939
|
-
_autoFlatten;
|
|
940
|
-
constructor(opts = {}) {
|
|
941
|
-
this._autoFlatten = opts.autoFlatten !== false;
|
|
942
|
-
}
|
|
943
|
-
register(def) {
|
|
944
|
-
if (!def.name) throw new Error("tool requires a name");
|
|
945
|
-
const internal = { ...def };
|
|
946
|
-
if (this._autoFlatten && def.parameters) {
|
|
947
|
-
const decision = analyzeSchema(def.parameters);
|
|
948
|
-
if (decision.shouldFlatten) {
|
|
949
|
-
internal.flatSchema = flattenSchema(def.parameters);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
this._tools.set(def.name, internal);
|
|
953
|
-
return this;
|
|
954
|
-
}
|
|
955
|
-
has(name) {
|
|
956
|
-
return this._tools.has(name);
|
|
957
|
-
}
|
|
958
|
-
get(name) {
|
|
959
|
-
return this._tools.get(name);
|
|
960
|
-
}
|
|
961
|
-
get size() {
|
|
962
|
-
return this._tools.size;
|
|
963
|
-
}
|
|
964
|
-
/** True if a registered tool's schema was flattened for the model. */
|
|
965
|
-
wasFlattened(name) {
|
|
966
|
-
return Boolean(this._tools.get(name)?.flatSchema);
|
|
967
|
-
}
|
|
968
|
-
specs() {
|
|
969
|
-
return [...this._tools.values()].map((t) => ({
|
|
970
|
-
type: "function",
|
|
971
|
-
function: {
|
|
972
|
-
name: t.name,
|
|
973
|
-
description: t.description ?? "",
|
|
974
|
-
parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
|
|
975
|
-
}
|
|
976
|
-
}));
|
|
977
|
-
}
|
|
978
|
-
async dispatch(name, argumentsRaw) {
|
|
979
|
-
const tool = this._tools.get(name);
|
|
980
|
-
if (!tool) {
|
|
981
|
-
return JSON.stringify({ error: `unknown tool: ${name}` });
|
|
982
|
-
}
|
|
983
|
-
let args;
|
|
984
|
-
try {
|
|
985
|
-
args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
|
|
986
|
-
} catch (err) {
|
|
987
|
-
return JSON.stringify({
|
|
988
|
-
error: `invalid tool arguments JSON: ${err.message}`
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
|
|
992
|
-
args = nestArguments(args);
|
|
993
|
-
}
|
|
994
|
-
try {
|
|
995
|
-
const result = await tool.fn(args);
|
|
996
|
-
return typeof result === "string" ? result : JSON.stringify(result);
|
|
997
|
-
} catch (err) {
|
|
998
|
-
return JSON.stringify({
|
|
999
|
-
error: `${err.name}: ${err.message}`
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
};
|
|
1004
|
-
function hasDotKey(obj) {
|
|
1005
|
-
for (const k of Object.keys(obj)) {
|
|
1006
|
-
if (k.includes(".")) return true;
|
|
1007
|
-
}
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
1106
|
// src/loop.ts
|
|
1012
1107
|
var CacheFirstLoop = class {
|
|
1013
1108
|
client;
|
|
@@ -1055,8 +1150,18 @@ var CacheFirstLoop = class {
|
|
|
1055
1150
|
this.sessionName = opts.session ?? null;
|
|
1056
1151
|
if (this.sessionName) {
|
|
1057
1152
|
const prior = loadSessionMessages(this.sessionName);
|
|
1058
|
-
|
|
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) {
|
|
@@ -2103,94 +2230,144 @@ function quoteArg(s, windows) {
|
|
|
2103
2230
|
return `"${s.replace(/"/g, '""')}"`;
|
|
2104
2231
|
}
|
|
2105
2232
|
|
|
2106
|
-
// src/mcp/
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2233
|
+
// src/mcp/sse.ts
|
|
2234
|
+
import { createParser as createParser2 } from "eventsource-parser";
|
|
2235
|
+
var SseTransport = class {
|
|
2236
|
+
url;
|
|
2237
|
+
headers;
|
|
2238
|
+
queue = [];
|
|
2239
|
+
waiters = [];
|
|
2240
|
+
controller = new AbortController();
|
|
2241
|
+
closed = false;
|
|
2242
|
+
postUrl = null;
|
|
2243
|
+
endpointReady;
|
|
2244
|
+
resolveEndpoint;
|
|
2245
|
+
rejectEndpoint;
|
|
2246
|
+
constructor(opts) {
|
|
2247
|
+
this.url = opts.url;
|
|
2248
|
+
this.headers = opts.headers ?? {};
|
|
2249
|
+
this.endpointReady = new Promise((resolve2, reject) => {
|
|
2250
|
+
this.resolveEndpoint = resolve2;
|
|
2251
|
+
this.rejectEndpoint = reject;
|
|
2252
|
+
});
|
|
2253
|
+
this.endpointReady.catch(() => void 0);
|
|
2254
|
+
void this.runStream();
|
|
2255
|
+
}
|
|
2256
|
+
async send(message) {
|
|
2257
|
+
if (this.closed) throw new Error("MCP SSE transport is closed");
|
|
2258
|
+
const postUrl = await this.endpointReady;
|
|
2259
|
+
const res = await fetch(postUrl, {
|
|
2260
|
+
method: "POST",
|
|
2261
|
+
headers: { "content-type": "application/json", ...this.headers },
|
|
2262
|
+
body: JSON.stringify(message),
|
|
2263
|
+
signal: this.controller.signal
|
|
2264
|
+
});
|
|
2265
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
2266
|
+
if (!res.ok) {
|
|
2267
|
+
throw new Error(`MCP SSE POST ${postUrl} failed: ${res.status} ${res.statusText}`);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
async *messages() {
|
|
2271
|
+
while (true) {
|
|
2272
|
+
if (this.queue.length > 0) {
|
|
2273
|
+
yield this.queue.shift();
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
if (this.closed) return;
|
|
2277
|
+
const next = await new Promise((resolve2) => {
|
|
2278
|
+
this.waiters.push(resolve2);
|
|
2279
|
+
});
|
|
2280
|
+
if (next === null) return;
|
|
2281
|
+
yield next;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
async close() {
|
|
2285
|
+
if (this.closed) return;
|
|
2286
|
+
this.closed = true;
|
|
2287
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2288
|
+
this.rejectEndpoint(new Error("MCP SSE transport closed before endpoint was ready"));
|
|
2289
|
+
try {
|
|
2290
|
+
this.controller.abort();
|
|
2291
|
+
} catch {
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
// ---------- internals ----------
|
|
2295
|
+
async runStream() {
|
|
2296
|
+
let res;
|
|
2297
|
+
try {
|
|
2298
|
+
res = await fetch(this.url, {
|
|
2299
|
+
method: "GET",
|
|
2300
|
+
headers: { accept: "text/event-stream", ...this.headers },
|
|
2301
|
+
signal: this.controller.signal
|
|
2302
|
+
});
|
|
2303
|
+
} catch (err) {
|
|
2304
|
+
this.failHandshake(`SSE connect to ${this.url} failed: ${err.message}`);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (!res.ok || !res.body) {
|
|
2308
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2309
|
+
this.failHandshake(`SSE handshake ${this.url} \u2192 ${res.status} ${res.statusText}`);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const parser = createParser2({
|
|
2313
|
+
onEvent: (ev) => this.handleEvent(ev.event ?? "message", ev.data)
|
|
2314
|
+
});
|
|
2315
|
+
const decoder = new TextDecoder();
|
|
2316
|
+
try {
|
|
2317
|
+
for await (const chunk of res.body) {
|
|
2318
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
2319
|
+
}
|
|
2320
|
+
} catch (err) {
|
|
2321
|
+
if (!this.closed) {
|
|
2322
|
+
this.pushError(`SSE stream error: ${err.message}`);
|
|
2323
|
+
}
|
|
2324
|
+
} finally {
|
|
2325
|
+
this.markClosed();
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
handleEvent(type, data) {
|
|
2329
|
+
if (type === "endpoint") {
|
|
2330
|
+
if (this.postUrl) return;
|
|
2331
|
+
try {
|
|
2332
|
+
this.postUrl = new URL(data, this.url).toString();
|
|
2333
|
+
this.resolveEndpoint(this.postUrl);
|
|
2334
|
+
} catch (err) {
|
|
2335
|
+
this.failHandshake(`SSE endpoint event had bad URL "${data}": ${err.message}`);
|
|
2336
|
+
}
|
|
2337
|
+
return;
|
|
2116
2338
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
fn: async (args) => {
|
|
2123
|
-
const toolResult = await client.callTool(mcpTool.name, args);
|
|
2124
|
-
return flattenMcpResult(toolResult);
|
|
2339
|
+
if (type === "message") {
|
|
2340
|
+
try {
|
|
2341
|
+
const parsed = JSON.parse(data);
|
|
2342
|
+
this.pushMessage(parsed);
|
|
2343
|
+
} catch {
|
|
2125
2344
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2128
2347
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
const joined = parts.join("\n").trim();
|
|
2134
|
-
if (result.isError) {
|
|
2135
|
-
return `ERROR: ${joined || "(no error message from server)"}`;
|
|
2348
|
+
failHandshake(reason) {
|
|
2349
|
+
this.rejectEndpoint(new Error(reason));
|
|
2350
|
+
this.pushError(reason);
|
|
2351
|
+
this.markClosed();
|
|
2136
2352
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
|
|
2142
|
-
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
// src/config.ts
|
|
2146
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2147
|
-
import { homedir as homedir2 } from "os";
|
|
2148
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
2149
|
-
function defaultConfigPath() {
|
|
2150
|
-
return join2(homedir2(), ".reasonix", "config.json");
|
|
2151
|
-
}
|
|
2152
|
-
function readConfig(path = defaultConfigPath()) {
|
|
2153
|
-
try {
|
|
2154
|
-
const raw = readFileSync4(path, "utf8");
|
|
2155
|
-
const parsed = JSON.parse(raw);
|
|
2156
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
2157
|
-
} catch {
|
|
2353
|
+
pushMessage(msg) {
|
|
2354
|
+
const waiter = this.waiters.shift();
|
|
2355
|
+
if (waiter) waiter(msg);
|
|
2356
|
+
else this.queue.push(msg);
|
|
2158
2357
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
chmodSync2(path, 384);
|
|
2166
|
-
} catch {
|
|
2358
|
+
pushError(message) {
|
|
2359
|
+
this.pushMessage({
|
|
2360
|
+
jsonrpc: "2.0",
|
|
2361
|
+
id: null,
|
|
2362
|
+
error: { code: -32e3, message }
|
|
2363
|
+
});
|
|
2167
2364
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
const cfg = readConfig(path);
|
|
2175
|
-
cfg.apiKey = key.trim();
|
|
2176
|
-
writeConfig(cfg, path);
|
|
2177
|
-
}
|
|
2178
|
-
function isPlausibleKey(key) {
|
|
2179
|
-
const trimmed = key.trim();
|
|
2180
|
-
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
2181
|
-
}
|
|
2182
|
-
function redactKey(key) {
|
|
2183
|
-
if (!key) return "";
|
|
2184
|
-
if (key.length <= 12) return "****";
|
|
2185
|
-
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// src/index.ts
|
|
2189
|
-
var VERSION = "0.3.0-alpha.3";
|
|
2190
|
-
|
|
2191
|
-
// src/cli/commands/chat.tsx
|
|
2192
|
-
import { render } from "ink";
|
|
2193
|
-
import React8, { useState as useState4 } from "react";
|
|
2365
|
+
markClosed() {
|
|
2366
|
+
if (this.closed) return;
|
|
2367
|
+
this.closed = true;
|
|
2368
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2194
2371
|
|
|
2195
2372
|
// src/mcp/shell-split.ts
|
|
2196
2373
|
function shellSplit(input) {
|
|
@@ -2243,6 +2420,7 @@ function shellSplit(input) {
|
|
|
2243
2420
|
|
|
2244
2421
|
// src/mcp/spec.ts
|
|
2245
2422
|
var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
|
|
2423
|
+
var HTTP_URL = /^https?:\/\//i;
|
|
2246
2424
|
function parseMcpSpec(input) {
|
|
2247
2425
|
const trimmed = input.trim();
|
|
2248
2426
|
if (!trimmed) {
|
|
@@ -2250,15 +2428,28 @@ function parseMcpSpec(input) {
|
|
|
2250
2428
|
}
|
|
2251
2429
|
const nameMatch = NAME_PREFIX.exec(trimmed);
|
|
2252
2430
|
const name = nameMatch ? nameMatch[1] : null;
|
|
2253
|
-
const body = nameMatch ? nameMatch[2] : trimmed;
|
|
2431
|
+
const body = (nameMatch ? nameMatch[2] : trimmed).trim();
|
|
2432
|
+
if (!body) {
|
|
2433
|
+
throw new Error(`MCP spec has name but no command: ${input}`);
|
|
2434
|
+
}
|
|
2435
|
+
if (HTTP_URL.test(body)) {
|
|
2436
|
+
return { transport: "sse", name, url: body };
|
|
2437
|
+
}
|
|
2254
2438
|
const argv = shellSplit(body);
|
|
2255
2439
|
if (argv.length === 0) {
|
|
2256
2440
|
throw new Error(`MCP spec has name but no command: ${input}`);
|
|
2257
2441
|
}
|
|
2258
2442
|
const [command, ...args] = argv;
|
|
2259
|
-
return { name, command, args };
|
|
2443
|
+
return { transport: "stdio", name, command, args };
|
|
2260
2444
|
}
|
|
2261
2445
|
|
|
2446
|
+
// src/index.ts
|
|
2447
|
+
var VERSION = "0.3.0-alpha.6";
|
|
2448
|
+
|
|
2449
|
+
// src/cli/commands/chat.tsx
|
|
2450
|
+
import { render } from "ink";
|
|
2451
|
+
import React8, { useState as useState4 } from "react";
|
|
2452
|
+
|
|
2262
2453
|
// src/cli/ui/App.tsx
|
|
2263
2454
|
import { Box as Box6, Static, Text as Text6, useApp } from "ink";
|
|
2264
2455
|
import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
@@ -2592,7 +2783,7 @@ function parseSlash(text) {
|
|
|
2592
2783
|
if (!cmd) return null;
|
|
2593
2784
|
return { cmd, args: parts.slice(1) };
|
|
2594
2785
|
}
|
|
2595
|
-
function handleSlash(cmd, args, loop) {
|
|
2786
|
+
function handleSlash(cmd, args, loop, ctx = {}) {
|
|
2596
2787
|
switch (cmd) {
|
|
2597
2788
|
case "exit":
|
|
2598
2789
|
case "quit":
|
|
@@ -2610,6 +2801,8 @@ function handleSlash(cmd, args, loop) {
|
|
|
2610
2801
|
" /model <id> deepseek-chat or deepseek-reasoner",
|
|
2611
2802
|
" /harvest [on|off] Pillar 2: structured plan-state extraction",
|
|
2612
2803
|
" /branch <N|off> run N parallel samples (N>=2), pick most confident",
|
|
2804
|
+
" /mcp list MCP servers + tools attached to this session",
|
|
2805
|
+
" /setup (exit + reconfigure) \u2192 run `reasonix setup`",
|
|
2613
2806
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
2614
2807
|
" /forget delete the current session from disk",
|
|
2615
2808
|
" /clear clear displayed history (log + session kept)",
|
|
@@ -2625,6 +2818,32 @@ function handleSlash(cmd, args, loop) {
|
|
|
2625
2818
|
" reasonix chat --no-session disable persistence for this run"
|
|
2626
2819
|
].join("\n")
|
|
2627
2820
|
};
|
|
2821
|
+
case "mcp": {
|
|
2822
|
+
const specs = ctx.mcpSpecs ?? [];
|
|
2823
|
+
const toolSpecs = loop.prefix.toolSpecs ?? [];
|
|
2824
|
+
if (specs.length === 0 && toolSpecs.length === 0) {
|
|
2825
|
+
return {
|
|
2826
|
+
info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
const lines = [];
|
|
2830
|
+
if (specs.length > 0) {
|
|
2831
|
+
lines.push(`MCP servers (${specs.length}):`);
|
|
2832
|
+
for (const spec of specs) lines.push(` \xB7 ${spec}`);
|
|
2833
|
+
lines.push("");
|
|
2834
|
+
}
|
|
2835
|
+
if (toolSpecs.length > 0) {
|
|
2836
|
+
lines.push(`Tools in registry (${toolSpecs.length}):`);
|
|
2837
|
+
for (const t of toolSpecs) lines.push(` \xB7 ${t.function.name}`);
|
|
2838
|
+
}
|
|
2839
|
+
lines.push("");
|
|
2840
|
+
lines.push("To change this set, exit and run `reasonix setup`.");
|
|
2841
|
+
return { info: lines.join("\n") };
|
|
2842
|
+
}
|
|
2843
|
+
case "setup":
|
|
2844
|
+
return {
|
|
2845
|
+
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
2846
|
+
};
|
|
2628
2847
|
case "sessions": {
|
|
2629
2848
|
const items = listSessions();
|
|
2630
2849
|
if (items.length === 0) {
|
|
@@ -2714,7 +2933,16 @@ function handleSlash(cmd, args, loop) {
|
|
|
2714
2933
|
|
|
2715
2934
|
// src/cli/ui/App.tsx
|
|
2716
2935
|
var FLUSH_INTERVAL_MS = 60;
|
|
2717
|
-
function App({
|
|
2936
|
+
function App({
|
|
2937
|
+
model,
|
|
2938
|
+
system,
|
|
2939
|
+
transcript,
|
|
2940
|
+
harvest: harvest2,
|
|
2941
|
+
branch,
|
|
2942
|
+
session,
|
|
2943
|
+
tools,
|
|
2944
|
+
mcpSpecs
|
|
2945
|
+
}) {
|
|
2718
2946
|
const { exit } = useApp();
|
|
2719
2947
|
const [historical, setHistorical] = useState2([]);
|
|
2720
2948
|
const [streaming, setStreaming] = useState2(null);
|
|
@@ -2802,7 +3030,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
|
|
|
2802
3030
|
setInput("");
|
|
2803
3031
|
const slash = parseSlash(text);
|
|
2804
3032
|
if (slash) {
|
|
2805
|
-
const result = handleSlash(slash.cmd, slash.args, loop);
|
|
3033
|
+
const result = handleSlash(slash.cmd, slash.args, loop, { mcpSpecs });
|
|
2806
3034
|
if (result.exit) {
|
|
2807
3035
|
transcriptRef.current?.end();
|
|
2808
3036
|
exit();
|
|
@@ -2913,7 +3141,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
|
|
|
2913
3141
|
setBusy(false);
|
|
2914
3142
|
}
|
|
2915
3143
|
},
|
|
2916
|
-
[busy, exit, loop, writeTranscript]
|
|
3144
|
+
[busy, exit, loop, mcpSpecs, writeTranscript]
|
|
2917
3145
|
);
|
|
2918
3146
|
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
|
|
2919
3147
|
StatsPanel,
|
|
@@ -2927,7 +3155,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
|
|
|
2927
3155
|
), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, null));
|
|
2928
3156
|
}
|
|
2929
3157
|
function CommandStrip() {
|
|
2930
|
-
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /
|
|
3158
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"));
|
|
2931
3159
|
}
|
|
2932
3160
|
function describeRepair(repair) {
|
|
2933
3161
|
const parts = [];
|
|
@@ -2977,7 +3205,7 @@ function Setup({ onReady }) {
|
|
|
2977
3205
|
}
|
|
2978
3206
|
|
|
2979
3207
|
// src/cli/commands/chat.tsx
|
|
2980
|
-
function Root({ initialKey, tools, ...appProps }) {
|
|
3208
|
+
function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
|
|
2981
3209
|
const [key, setKey] = useState4(initialKey);
|
|
2982
3210
|
if (!key) {
|
|
2983
3211
|
return /* @__PURE__ */ React8.createElement(
|
|
@@ -3000,43 +3228,56 @@ function Root({ initialKey, tools, ...appProps }) {
|
|
|
3000
3228
|
harvest: appProps.harvest,
|
|
3001
3229
|
branch: appProps.branch,
|
|
3002
3230
|
session: appProps.session,
|
|
3003
|
-
tools
|
|
3231
|
+
tools,
|
|
3232
|
+
mcpSpecs
|
|
3004
3233
|
}
|
|
3005
3234
|
);
|
|
3006
3235
|
}
|
|
3007
3236
|
async function chatCommand(opts) {
|
|
3008
3237
|
loadDotenv();
|
|
3009
3238
|
const initialKey = loadApiKey();
|
|
3010
|
-
const
|
|
3239
|
+
const requestedSpecs = opts.mcp ?? [];
|
|
3011
3240
|
const clients = [];
|
|
3241
|
+
const successfulSpecs = [];
|
|
3242
|
+
const failedSpecs = [];
|
|
3012
3243
|
let tools;
|
|
3013
|
-
if (
|
|
3244
|
+
if (requestedSpecs.length > 0) {
|
|
3014
3245
|
tools = new ToolRegistry();
|
|
3015
|
-
for (const raw of
|
|
3246
|
+
for (const raw of requestedSpecs) {
|
|
3016
3247
|
try {
|
|
3017
3248
|
const spec = parseMcpSpec(raw);
|
|
3018
|
-
const prefix = spec.name ? `${spec.name}_` :
|
|
3019
|
-
const transport = new StdioTransport({ command: spec.command, args: spec.args });
|
|
3249
|
+
const prefix = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3250
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3020
3251
|
const mcp2 = new McpClient({ transport });
|
|
3021
3252
|
await mcp2.initialize();
|
|
3022
3253
|
const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix });
|
|
3023
3254
|
const label = spec.name ?? "anon";
|
|
3255
|
+
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
3024
3256
|
process.stderr.write(
|
|
3025
|
-
`\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${
|
|
3257
|
+
`\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
3026
3258
|
`
|
|
3027
3259
|
);
|
|
3028
3260
|
clients.push(mcp2);
|
|
3261
|
+
successfulSpecs.push(raw);
|
|
3029
3262
|
} catch (err) {
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3263
|
+
const reason = err.message;
|
|
3264
|
+
failedSpecs.push({ spec: raw, reason });
|
|
3265
|
+
process.stderr.write(
|
|
3266
|
+
`\u25B8 MCP setup SKIPPED for "${raw}": ${reason}
|
|
3267
|
+
\u2192 this server will not be available this session. Run \`reasonix setup\` to remove it, or fix the underlying issue (missing npm package, network, etc.).
|
|
3268
|
+
`
|
|
3269
|
+
);
|
|
3034
3270
|
}
|
|
3035
3271
|
}
|
|
3272
|
+
if (successfulSpecs.length === 0) {
|
|
3273
|
+
tools = void 0;
|
|
3274
|
+
}
|
|
3036
3275
|
}
|
|
3037
|
-
const
|
|
3038
|
-
|
|
3039
|
-
|
|
3276
|
+
const mcpSpecs = successfulSpecs;
|
|
3277
|
+
const { waitUntilExit } = render(
|
|
3278
|
+
/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, mcpSpecs, ...opts }),
|
|
3279
|
+
{ exitOnCtrlC: true }
|
|
3280
|
+
);
|
|
3040
3281
|
try {
|
|
3041
3282
|
await waitUntilExit();
|
|
3042
3283
|
} finally {
|
|
@@ -3215,11 +3456,6 @@ var MCP_CATALOG = [
|
|
|
3215
3456
|
userArgs: "<dir>",
|
|
3216
3457
|
note: "the directory is a hard sandbox \u2014 the server refuses access outside it"
|
|
3217
3458
|
},
|
|
3218
|
-
{
|
|
3219
|
-
name: "fetch",
|
|
3220
|
-
summary: "fetch URLs (markdown-friendly extraction, not a full browser)",
|
|
3221
|
-
package: "@modelcontextprotocol/server-fetch"
|
|
3222
|
-
},
|
|
3223
3459
|
{
|
|
3224
3460
|
name: "memory",
|
|
3225
3461
|
summary: "persistent key-value memory across sessions",
|
|
@@ -3231,12 +3467,6 @@ var MCP_CATALOG = [
|
|
|
3231
3467
|
package: "@modelcontextprotocol/server-github",
|
|
3232
3468
|
note: "set GITHUB_PERSONAL_ACCESS_TOKEN in your env before spawning"
|
|
3233
3469
|
},
|
|
3234
|
-
{
|
|
3235
|
-
name: "sqlite",
|
|
3236
|
-
summary: "read/write a sqlite database file",
|
|
3237
|
-
package: "@modelcontextprotocol/server-sqlite",
|
|
3238
|
-
userArgs: "<db.sqlite>"
|
|
3239
|
-
},
|
|
3240
3470
|
{
|
|
3241
3471
|
name: "puppeteer",
|
|
3242
3472
|
summary: "browser automation \u2014 take screenshots, click, type",
|
|
@@ -3472,32 +3702,36 @@ async function runCommand(opts) {
|
|
|
3472
3702
|
loadDotenv();
|
|
3473
3703
|
const apiKey = await ensureApiKey();
|
|
3474
3704
|
process.env.DEEPSEEK_API_KEY = apiKey;
|
|
3475
|
-
const
|
|
3705
|
+
const requestedSpecs = opts.mcp ?? [];
|
|
3476
3706
|
const clients = [];
|
|
3477
3707
|
let tools;
|
|
3478
|
-
|
|
3708
|
+
let successCount = 0;
|
|
3709
|
+
if (requestedSpecs.length > 0) {
|
|
3479
3710
|
tools = new ToolRegistry();
|
|
3480
|
-
for (const raw of
|
|
3711
|
+
for (const raw of requestedSpecs) {
|
|
3481
3712
|
try {
|
|
3482
3713
|
const spec = parseMcpSpec(raw);
|
|
3483
|
-
const prefix2 = spec.name ? `${spec.name}_` :
|
|
3484
|
-
const
|
|
3485
|
-
|
|
3486
|
-
});
|
|
3714
|
+
const prefix2 = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3715
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3716
|
+
const mcp2 = new McpClient({ transport });
|
|
3487
3717
|
await mcp2.initialize();
|
|
3488
3718
|
const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix2 });
|
|
3719
|
+
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
3489
3720
|
process.stderr.write(
|
|
3490
|
-
`\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${
|
|
3721
|
+
`\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
3491
3722
|
`
|
|
3492
3723
|
);
|
|
3493
3724
|
clients.push(mcp2);
|
|
3725
|
+
successCount++;
|
|
3494
3726
|
} catch (err) {
|
|
3495
|
-
process.stderr.write(
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3727
|
+
process.stderr.write(
|
|
3728
|
+
`\u25B8 MCP setup SKIPPED for "${raw}": ${err.message}
|
|
3729
|
+
\u2192 run \`reasonix setup\` to remove broken entries from your saved config.
|
|
3730
|
+
`
|
|
3731
|
+
);
|
|
3499
3732
|
}
|
|
3500
3733
|
}
|
|
3734
|
+
if (successCount === 0) tools = void 0;
|
|
3501
3735
|
}
|
|
3502
3736
|
const client = new DeepSeekClient();
|
|
3503
3737
|
const prefix = new ImmutablePrefix({
|
|
@@ -3637,6 +3871,404 @@ function truncate4(s, max) {
|
|
|
3637
3871
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
|
|
3638
3872
|
}
|
|
3639
3873
|
|
|
3874
|
+
// src/cli/commands/setup.tsx
|
|
3875
|
+
import { render as render4 } from "ink";
|
|
3876
|
+
import React16 from "react";
|
|
3877
|
+
|
|
3878
|
+
// src/cli/ui/Wizard.tsx
|
|
3879
|
+
import { Box as Box12, Text as Text12, useApp as useApp5, useInput as useInput4 } from "ink";
|
|
3880
|
+
import TextInput3 from "ink-text-input";
|
|
3881
|
+
import React15, { useState as useState8 } from "react";
|
|
3882
|
+
|
|
3883
|
+
// src/cli/ui/Select.tsx
|
|
3884
|
+
import { Box as Box11, Text as Text11, useInput as useInput3 } from "ink";
|
|
3885
|
+
import React14, { useState as useState7 } from "react";
|
|
3886
|
+
function SingleSelect({
|
|
3887
|
+
items,
|
|
3888
|
+
initialValue,
|
|
3889
|
+
onSubmit,
|
|
3890
|
+
onCancel
|
|
3891
|
+
}) {
|
|
3892
|
+
const initialIndex = Math.max(
|
|
3893
|
+
0,
|
|
3894
|
+
items.findIndex((i) => i.value === initialValue && !i.disabled)
|
|
3895
|
+
);
|
|
3896
|
+
const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
|
|
3897
|
+
useInput3((_input, key) => {
|
|
3898
|
+
if (key.upArrow) {
|
|
3899
|
+
setIndex((i) => findNextEnabled(items, i, -1));
|
|
3900
|
+
} else if (key.downArrow) {
|
|
3901
|
+
setIndex((i) => findNextEnabled(items, i, 1));
|
|
3902
|
+
} else if (key.return) {
|
|
3903
|
+
const chosen = items[index];
|
|
3904
|
+
if (chosen && !chosen.disabled) onSubmit(chosen.value);
|
|
3905
|
+
} else if (key.escape && onCancel) {
|
|
3906
|
+
onCancel();
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3909
|
+
return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React14.createElement(
|
|
3910
|
+
SelectRow,
|
|
3911
|
+
{
|
|
3912
|
+
key: item.value,
|
|
3913
|
+
item,
|
|
3914
|
+
active: i === index,
|
|
3915
|
+
marker: i === index ? "\u25B8" : " "
|
|
3916
|
+
}
|
|
3917
|
+
)));
|
|
3918
|
+
}
|
|
3919
|
+
function MultiSelect({
|
|
3920
|
+
items,
|
|
3921
|
+
initialSelected = [],
|
|
3922
|
+
onSubmit,
|
|
3923
|
+
onCancel,
|
|
3924
|
+
footer
|
|
3925
|
+
}) {
|
|
3926
|
+
const [index, setIndex] = useState7(() => {
|
|
3927
|
+
const first = items.findIndex((i) => !i.disabled);
|
|
3928
|
+
return first === -1 ? 0 : first;
|
|
3929
|
+
});
|
|
3930
|
+
const [selected, setSelected] = useState7(new Set(initialSelected));
|
|
3931
|
+
useInput3((input, key) => {
|
|
3932
|
+
if (key.upArrow) {
|
|
3933
|
+
setIndex((i) => findNextEnabled(items, i, -1));
|
|
3934
|
+
} else if (key.downArrow) {
|
|
3935
|
+
setIndex((i) => findNextEnabled(items, i, 1));
|
|
3936
|
+
} else if (input === " ") {
|
|
3937
|
+
const item = items[index];
|
|
3938
|
+
if (!item || item.disabled) return;
|
|
3939
|
+
setSelected((prev) => {
|
|
3940
|
+
const next = new Set(prev);
|
|
3941
|
+
if (next.has(item.value)) next.delete(item.value);
|
|
3942
|
+
else next.add(item.value);
|
|
3943
|
+
return next;
|
|
3944
|
+
});
|
|
3945
|
+
} else if (key.return) {
|
|
3946
|
+
const ordered = items.filter((i) => selected.has(i.value)).map((i) => i.value);
|
|
3947
|
+
onSubmit(ordered);
|
|
3948
|
+
} else if (key.escape && onCancel) {
|
|
3949
|
+
onCancel();
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3952
|
+
return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => {
|
|
3953
|
+
const checked = selected.has(item.value);
|
|
3954
|
+
const marker = checked ? "[x]" : "[ ]";
|
|
3955
|
+
return /* @__PURE__ */ React14.createElement(
|
|
3956
|
+
SelectRow,
|
|
3957
|
+
{
|
|
3958
|
+
key: item.value,
|
|
3959
|
+
item,
|
|
3960
|
+
active: i === index,
|
|
3961
|
+
marker: `${i === index ? "\u25B8" : " "} ${marker}`
|
|
3962
|
+
}
|
|
3963
|
+
);
|
|
3964
|
+
}), footer ? /* @__PURE__ */ React14.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, footer)) : null);
|
|
3965
|
+
}
|
|
3966
|
+
function SelectRow({
|
|
3967
|
+
item,
|
|
3968
|
+
active,
|
|
3969
|
+
marker
|
|
3970
|
+
}) {
|
|
3971
|
+
const color = item.disabled ? "gray" : active ? "cyan" : void 0;
|
|
3972
|
+
return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React14.createElement(Box11, null, /* @__PURE__ */ React14.createElement(Text11, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React14.createElement(Box11, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, item.hint)) : null);
|
|
3973
|
+
}
|
|
3974
|
+
function findNextEnabled(items, from, step) {
|
|
3975
|
+
if (items.length === 0) return 0;
|
|
3976
|
+
let i = from;
|
|
3977
|
+
for (let tries = 0; tries < items.length; tries++) {
|
|
3978
|
+
i = (i + step + items.length) % items.length;
|
|
3979
|
+
if (!items[i]?.disabled) return i;
|
|
3980
|
+
}
|
|
3981
|
+
return from;
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
// src/cli/ui/presets.ts
|
|
3985
|
+
var PRESETS = {
|
|
3986
|
+
fast: { model: "deepseek-chat", harvest: false, branch: 1 },
|
|
3987
|
+
smart: { model: "deepseek-reasoner", harvest: true, branch: 1 },
|
|
3988
|
+
max: { model: "deepseek-reasoner", harvest: true, branch: 3 }
|
|
3989
|
+
};
|
|
3990
|
+
var PRESET_DESCRIPTIONS = {
|
|
3991
|
+
fast: {
|
|
3992
|
+
headline: "deepseek-chat, no reasoning harvest, no branching",
|
|
3993
|
+
cost: "~1\xA2 per 100 turns \xB7 default"
|
|
3994
|
+
},
|
|
3995
|
+
smart: {
|
|
3996
|
+
headline: "deepseek-reasoner + Pillar 2 harvest",
|
|
3997
|
+
cost: "~10\xD7 cost vs fast \xB7 slower \xB7 better on multi-step tasks"
|
|
3998
|
+
},
|
|
3999
|
+
max: {
|
|
4000
|
+
headline: "reasoner + harvest + self-consistency (3 branches)",
|
|
4001
|
+
cost: "~30\xD7 cost vs fast \xB7 slowest \xB7 for hard single-shots"
|
|
4002
|
+
}
|
|
4003
|
+
};
|
|
4004
|
+
|
|
4005
|
+
// src/cli/ui/Wizard.tsx
|
|
4006
|
+
var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
|
|
4007
|
+
function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
4008
|
+
const { exit } = useApp5();
|
|
4009
|
+
const [step, setStep] = useState8(existingApiKey ? "preset" : "apiKey");
|
|
4010
|
+
const [data, setData] = useState8({
|
|
4011
|
+
apiKey: existingApiKey ?? "",
|
|
4012
|
+
preset: initial?.preset ?? "fast",
|
|
4013
|
+
selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
|
|
4014
|
+
catalogArgs: {}
|
|
4015
|
+
});
|
|
4016
|
+
const [error, setError] = useState8(null);
|
|
4017
|
+
useInput4((_input, key) => {
|
|
4018
|
+
if (key.escape && step !== "saved" && onCancel) onCancel();
|
|
4019
|
+
});
|
|
4020
|
+
if (step === "apiKey") {
|
|
4021
|
+
return /* @__PURE__ */ React15.createElement(
|
|
4022
|
+
ApiKeyStep,
|
|
4023
|
+
{
|
|
4024
|
+
onSubmit: (key) => {
|
|
4025
|
+
setData((d) => ({ ...d, apiKey: key }));
|
|
4026
|
+
setError(null);
|
|
4027
|
+
setStep("preset");
|
|
4028
|
+
},
|
|
4029
|
+
error,
|
|
4030
|
+
onError: setError
|
|
4031
|
+
}
|
|
4032
|
+
);
|
|
4033
|
+
}
|
|
4034
|
+
if (step === "preset") {
|
|
4035
|
+
return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React15.createElement(
|
|
4036
|
+
SingleSelect,
|
|
4037
|
+
{
|
|
4038
|
+
items: presetItems(),
|
|
4039
|
+
initialValue: data.preset,
|
|
4040
|
+
onSubmit: (preset) => {
|
|
4041
|
+
setData((d) => ({ ...d, preset }));
|
|
4042
|
+
setStep("mcp");
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
|
|
4046
|
+
}
|
|
4047
|
+
if (step === "mcp") {
|
|
4048
|
+
return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React15.createElement(
|
|
4049
|
+
MultiSelect,
|
|
4050
|
+
{
|
|
4051
|
+
items: mcpItems(),
|
|
4052
|
+
initialSelected: data.selectedCatalog,
|
|
4053
|
+
onSubmit: (selected) => {
|
|
4054
|
+
setData((d) => ({ ...d, selectedCatalog: selected }));
|
|
4055
|
+
const needsArgs = selected.some((name) => CATALOG_BY_NAME.get(name)?.userArgs);
|
|
4056
|
+
setStep(needsArgs ? "mcpArgs" : "review");
|
|
4057
|
+
},
|
|
4058
|
+
footer: "\u2191/\u2193 move \xB7 space toggle \xB7 enter confirm \xB7 esc cancel \xB7 leave empty to skip"
|
|
4059
|
+
}
|
|
4060
|
+
));
|
|
4061
|
+
}
|
|
4062
|
+
if (step === "mcpArgs") {
|
|
4063
|
+
const pending = data.selectedCatalog.filter((name) => {
|
|
4064
|
+
const entry2 = CATALOG_BY_NAME.get(name);
|
|
4065
|
+
return entry2?.userArgs && !data.catalogArgs[name];
|
|
4066
|
+
});
|
|
4067
|
+
if (pending.length === 0) {
|
|
4068
|
+
setStep("review");
|
|
4069
|
+
return null;
|
|
4070
|
+
}
|
|
4071
|
+
const currentName = pending[0];
|
|
4072
|
+
const entry = CATALOG_BY_NAME.get(currentName);
|
|
4073
|
+
return /* @__PURE__ */ React15.createElement(
|
|
4074
|
+
McpArgsStep,
|
|
4075
|
+
{
|
|
4076
|
+
entry,
|
|
4077
|
+
error,
|
|
4078
|
+
onSubmit: (value) => {
|
|
4079
|
+
setData((d) => ({
|
|
4080
|
+
...d,
|
|
4081
|
+
catalogArgs: { ...d.catalogArgs, [currentName]: value }
|
|
4082
|
+
}));
|
|
4083
|
+
setError(null);
|
|
4084
|
+
},
|
|
4085
|
+
onError: setError
|
|
4086
|
+
}
|
|
4087
|
+
);
|
|
4088
|
+
}
|
|
4089
|
+
if (step === "review") {
|
|
4090
|
+
const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
|
|
4091
|
+
return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React15.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React15.createElement(
|
|
4092
|
+
SummaryLine,
|
|
4093
|
+
{
|
|
4094
|
+
label: "MCP",
|
|
4095
|
+
value: specs.length === 0 ? "(none)" : `${specs.length} server(s)`
|
|
4096
|
+
}
|
|
4097
|
+
), specs.map((spec, i) => (
|
|
4098
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
|
|
4099
|
+
/* @__PURE__ */ React15.createElement(Box12, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\xB7 ", spec))
|
|
4100
|
+
)), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null, /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "enter save \xB7 esc cancel"))), /* @__PURE__ */ React15.createElement(
|
|
4101
|
+
ReviewConfirm,
|
|
4102
|
+
{
|
|
4103
|
+
onConfirm: () => {
|
|
4104
|
+
try {
|
|
4105
|
+
const specsNow = data.selectedCatalog.map(
|
|
4106
|
+
(name) => buildSpec(name, data.catalogArgs)
|
|
4107
|
+
);
|
|
4108
|
+
const prev = readConfig();
|
|
4109
|
+
const next = {
|
|
4110
|
+
...prev,
|
|
4111
|
+
apiKey: data.apiKey,
|
|
4112
|
+
preset: data.preset,
|
|
4113
|
+
mcp: specsNow,
|
|
4114
|
+
setupCompleted: true
|
|
4115
|
+
};
|
|
4116
|
+
writeConfig(next);
|
|
4117
|
+
setStep("saved");
|
|
4118
|
+
onComplete(next);
|
|
4119
|
+
} catch (e) {
|
|
4120
|
+
setError(`Could not save config: ${e.message}`);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
));
|
|
4125
|
+
}
|
|
4126
|
+
return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Press enter to exit.")), /* @__PURE__ */ React15.createElement(ExitOnEnter, { onExit: exit }));
|
|
4127
|
+
}
|
|
4128
|
+
function ApiKeyStep({
|
|
4129
|
+
onSubmit,
|
|
4130
|
+
error,
|
|
4131
|
+
onError
|
|
4132
|
+
}) {
|
|
4133
|
+
const [value, setValue] = useState8("");
|
|
4134
|
+
return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React15.createElement(
|
|
4135
|
+
TextInput3,
|
|
4136
|
+
{
|
|
4137
|
+
value,
|
|
4138
|
+
onChange: setValue,
|
|
4139
|
+
onSubmit: (raw) => {
|
|
4140
|
+
const trimmed = raw.trim();
|
|
4141
|
+
if (!isPlausibleKey(trimmed)) {
|
|
4142
|
+
onError("Doesn't look like a DeepSeek key. They start with 'sk-' and are 30+ chars.");
|
|
4143
|
+
setValue("");
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
4146
|
+
onSubmit(trimmed);
|
|
4147
|
+
},
|
|
4148
|
+
mask: "\u2022",
|
|
4149
|
+
placeholder: "sk-..."
|
|
4150
|
+
}
|
|
4151
|
+
)), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : value ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "preview: ", redactKey(value))) : null);
|
|
4152
|
+
}
|
|
4153
|
+
function McpArgsStep({
|
|
4154
|
+
entry,
|
|
4155
|
+
error,
|
|
4156
|
+
onSubmit,
|
|
4157
|
+
onError
|
|
4158
|
+
}) {
|
|
4159
|
+
const [value, setValue] = useState8("");
|
|
4160
|
+
return /* @__PURE__ */ React15.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column" }, /* @__PURE__ */ React15.createElement(Text12, null, entry.summary), entry.note ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, null, "Required parameter: "), /* @__PURE__ */ React15.createElement(Text12, { bold: true }, entry.userArgs)), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React15.createElement(
|
|
4161
|
+
TextInput3,
|
|
4162
|
+
{
|
|
4163
|
+
value,
|
|
4164
|
+
onChange: setValue,
|
|
4165
|
+
onSubmit: (raw) => {
|
|
4166
|
+
const trimmed = raw.trim();
|
|
4167
|
+
if (!trimmed) {
|
|
4168
|
+
onError(`${entry.name} needs a value \u2014 got an empty string.`);
|
|
4169
|
+
return;
|
|
4170
|
+
}
|
|
4171
|
+
onSubmit(trimmed);
|
|
4172
|
+
setValue("");
|
|
4173
|
+
},
|
|
4174
|
+
placeholder: placeholderFor(entry)
|
|
4175
|
+
}
|
|
4176
|
+
)), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null));
|
|
4177
|
+
}
|
|
4178
|
+
function ReviewConfirm({ onConfirm }) {
|
|
4179
|
+
useInput4((_i, key) => {
|
|
4180
|
+
if (key.return) onConfirm();
|
|
4181
|
+
});
|
|
4182
|
+
return null;
|
|
4183
|
+
}
|
|
4184
|
+
function ExitOnEnter({ onExit }) {
|
|
4185
|
+
useInput4((_i, key) => {
|
|
4186
|
+
if (key.return) onExit();
|
|
4187
|
+
});
|
|
4188
|
+
return null;
|
|
4189
|
+
}
|
|
4190
|
+
function StepFrame({
|
|
4191
|
+
title,
|
|
4192
|
+
step,
|
|
4193
|
+
total,
|
|
4194
|
+
children
|
|
4195
|
+
}) {
|
|
4196
|
+
return /* @__PURE__ */ React15.createElement(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React15.createElement(Text12, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1, flexDirection: "column" }, children));
|
|
4197
|
+
}
|
|
4198
|
+
function SummaryLine({ label, value }) {
|
|
4199
|
+
return /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, null, label.padEnd(12)), /* @__PURE__ */ React15.createElement(Text12, { bold: true }, value));
|
|
4200
|
+
}
|
|
4201
|
+
function presetItems() {
|
|
4202
|
+
return ["fast", "smart", "max"].map((name) => ({
|
|
4203
|
+
value: name,
|
|
4204
|
+
label: `${name} \u2014 ${PRESET_DESCRIPTIONS[name].headline}`,
|
|
4205
|
+
hint: PRESET_DESCRIPTIONS[name].cost
|
|
4206
|
+
}));
|
|
4207
|
+
}
|
|
4208
|
+
function mcpItems() {
|
|
4209
|
+
return MCP_CATALOG.map((entry) => {
|
|
4210
|
+
const hintParts = [entry.summary];
|
|
4211
|
+
if (entry.userArgs) hintParts.push(`(you'll provide ${entry.userArgs})`);
|
|
4212
|
+
if (entry.note) hintParts.push(entry.note);
|
|
4213
|
+
return {
|
|
4214
|
+
value: entry.name,
|
|
4215
|
+
label: entry.name,
|
|
4216
|
+
hint: hintParts.join(" \xB7 ")
|
|
4217
|
+
};
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
function placeholderFor(entry) {
|
|
4221
|
+
if (entry.name === "filesystem") return "e.g. /tmp/reasonix-sandbox";
|
|
4222
|
+
if (entry.name === "sqlite") return "e.g. ./notes.sqlite";
|
|
4223
|
+
return entry.userArgs ?? "";
|
|
4224
|
+
}
|
|
4225
|
+
function deriveInitialCatalog(existingSpecs) {
|
|
4226
|
+
const packageToName = new Map(MCP_CATALOG.map((e) => [e.package, e.name]));
|
|
4227
|
+
const out = [];
|
|
4228
|
+
for (const spec of existingSpecs) {
|
|
4229
|
+
for (const [pkg, name] of packageToName) {
|
|
4230
|
+
if (spec.includes(pkg)) {
|
|
4231
|
+
out.push(name);
|
|
4232
|
+
break;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
return out;
|
|
4237
|
+
}
|
|
4238
|
+
function buildSpec(name, argsByName) {
|
|
4239
|
+
const entry = CATALOG_BY_NAME.get(name);
|
|
4240
|
+
if (!entry) return name;
|
|
4241
|
+
const userArg = entry.userArgs ? argsByName[name] : void 0;
|
|
4242
|
+
const tail = userArg ? ` ${quoteIfNeeded(userArg)}` : "";
|
|
4243
|
+
return `${entry.name}=npx -y ${entry.package}${tail}`;
|
|
4244
|
+
}
|
|
4245
|
+
function quoteIfNeeded(s) {
|
|
4246
|
+
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
4247
|
+
}
|
|
4248
|
+
|
|
4249
|
+
// src/cli/commands/setup.tsx
|
|
4250
|
+
async function setupCommand(_opts = {}) {
|
|
4251
|
+
loadDotenv();
|
|
4252
|
+
const existingKey = loadApiKey();
|
|
4253
|
+
const existing = readConfig();
|
|
4254
|
+
const { waitUntilExit, unmount } = render4(
|
|
4255
|
+
/* @__PURE__ */ React16.createElement(
|
|
4256
|
+
Wizard,
|
|
4257
|
+
{
|
|
4258
|
+
existingApiKey: existingKey,
|
|
4259
|
+
initial: { preset: existing.preset, mcp: existing.mcp },
|
|
4260
|
+
onComplete: () => {
|
|
4261
|
+
},
|
|
4262
|
+
onCancel: () => {
|
|
4263
|
+
unmount();
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
),
|
|
4267
|
+
{ exitOnCtrlC: true }
|
|
4268
|
+
);
|
|
4269
|
+
await waitUntilExit();
|
|
4270
|
+
}
|
|
4271
|
+
|
|
3640
4272
|
// src/cli/commands/stats.ts
|
|
3641
4273
|
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
3642
4274
|
function statsCommand(opts) {
|
|
@@ -3668,72 +4300,131 @@ function versionCommand() {
|
|
|
3668
4300
|
console.log(`reasonix ${VERSION}`);
|
|
3669
4301
|
}
|
|
3670
4302
|
|
|
4303
|
+
// src/cli/resolve.ts
|
|
4304
|
+
function resolveDefaults(flags) {
|
|
4305
|
+
const cfg = flags.noConfig ? {} : readConfig();
|
|
4306
|
+
const preset = pickPreset(flags.preset, cfg.preset);
|
|
4307
|
+
const presetSettings = PRESETS[preset];
|
|
4308
|
+
const model = flags.model ?? presetSettings.model;
|
|
4309
|
+
const harvest2 = flags.harvest === true ? true : presetSettings.harvest;
|
|
4310
|
+
const branchFromFlag = normalizeBranch(flags.branch);
|
|
4311
|
+
const branch = branchFromFlag ?? (presetSettings.branch > 1 ? presetSettings.branch : void 0);
|
|
4312
|
+
const mcp2 = flags.mcp && flags.mcp.length > 0 ? flags.mcp : cfg.mcp ?? [];
|
|
4313
|
+
const session = resolveSession(flags.session, cfg.session);
|
|
4314
|
+
return { model, harvest: harvest2, branch, mcp: mcp2, session };
|
|
4315
|
+
}
|
|
4316
|
+
function pickPreset(flagPreset, configPreset) {
|
|
4317
|
+
if (flagPreset && isPresetName(flagPreset)) return flagPreset;
|
|
4318
|
+
if (configPreset) return configPreset;
|
|
4319
|
+
return "fast";
|
|
4320
|
+
}
|
|
4321
|
+
function isPresetName(s) {
|
|
4322
|
+
return s === "fast" || s === "smart" || s === "max";
|
|
4323
|
+
}
|
|
4324
|
+
function normalizeBranch(raw) {
|
|
4325
|
+
if (raw === void 0) return void 0;
|
|
4326
|
+
if (!Number.isFinite(raw) || raw <= 1) return void 0;
|
|
4327
|
+
return Math.min(raw, 8);
|
|
4328
|
+
}
|
|
4329
|
+
function resolveSession(flag, configSession) {
|
|
4330
|
+
if (flag === false) return void 0;
|
|
4331
|
+
if (typeof flag === "string" && flag.length > 0) return flag;
|
|
4332
|
+
if (configSession === null) return void 0;
|
|
4333
|
+
if (typeof configSession === "string" && configSession.length > 0) return configSession;
|
|
4334
|
+
return "default";
|
|
4335
|
+
}
|
|
4336
|
+
|
|
3671
4337
|
// src/cli/index.ts
|
|
3672
4338
|
var DEFAULT_SYSTEM = "You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.";
|
|
3673
4339
|
var program = new Command();
|
|
3674
4340
|
program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
|
|
3675
|
-
program.
|
|
4341
|
+
program.action(async () => {
|
|
4342
|
+
const cfg = readConfig();
|
|
4343
|
+
if (!cfg.setupCompleted) {
|
|
4344
|
+
await setupCommand({});
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
const defaults = resolveDefaults({});
|
|
4348
|
+
await chatCommand({
|
|
4349
|
+
model: defaults.model,
|
|
4350
|
+
system: DEFAULT_SYSTEM,
|
|
4351
|
+
harvest: defaults.harvest,
|
|
4352
|
+
branch: defaults.branch,
|
|
4353
|
+
session: defaults.session,
|
|
4354
|
+
mcp: defaults.mcp
|
|
4355
|
+
});
|
|
4356
|
+
});
|
|
4357
|
+
program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
|
|
4358
|
+
await setupCommand({});
|
|
4359
|
+
});
|
|
4360
|
+
program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
|
|
4361
|
+
"--preset <name>",
|
|
4362
|
+
"Bundle of model + harvest + branch. One of: fast, smart, max. Overrides config.preset."
|
|
4363
|
+
).option(
|
|
3676
4364
|
"--harvest",
|
|
3677
|
-
"Extract typed plan state from R1 reasoning (Pillar 2
|
|
4365
|
+
"Extract typed plan state from R1 reasoning (Pillar 2). Overrides preset's harvest setting."
|
|
3678
4366
|
).option(
|
|
3679
4367
|
"--branch <n>",
|
|
3680
4368
|
"Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
|
|
3681
4369
|
(v) => Number.parseInt(v, 10)
|
|
3682
|
-
).option(
|
|
3683
|
-
"--session <name>",
|
|
3684
|
-
"Use a named session (default: 'default'). Resume the same session next time."
|
|
3685
|
-
).option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
|
|
4370
|
+
).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
|
|
3686
4371
|
"--mcp <spec>",
|
|
3687
|
-
'MCP server spec; repeatable.
|
|
4372
|
+
'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE transport). Overrides config.mcp when provided.',
|
|
3688
4373
|
(value, previous = []) => [...previous, value],
|
|
3689
4374
|
[]
|
|
3690
4375
|
).option(
|
|
3691
4376
|
"--mcp-prefix <str>",
|
|
3692
4377
|
"Global prefix applied to every MCP tool (only honored when no per-spec name is set; avoids collisions with a single anonymous server)"
|
|
3693
|
-
).action(async (opts) => {
|
|
3694
|
-
|
|
3695
|
-
if (opts.session === false) {
|
|
3696
|
-
session = void 0;
|
|
3697
|
-
} else if (typeof opts.session === "string" && opts.session.length > 0) {
|
|
3698
|
-
session = opts.session;
|
|
3699
|
-
} else {
|
|
3700
|
-
session = "default";
|
|
3701
|
-
}
|
|
3702
|
-
await chatCommand({
|
|
4378
|
+
).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (opts) => {
|
|
4379
|
+
const defaults = resolveDefaults({
|
|
3703
4380
|
model: opts.model,
|
|
4381
|
+
harvest: opts.harvest,
|
|
4382
|
+
branch: opts.branch,
|
|
4383
|
+
mcp: opts.mcp,
|
|
4384
|
+
session: opts.session,
|
|
4385
|
+
preset: opts.preset,
|
|
4386
|
+
noConfig: opts.config === false
|
|
4387
|
+
});
|
|
4388
|
+
await chatCommand({
|
|
4389
|
+
model: defaults.model,
|
|
3704
4390
|
system: opts.system,
|
|
3705
4391
|
transcript: opts.transcript,
|
|
3706
|
-
harvest:
|
|
3707
|
-
branch:
|
|
3708
|
-
session,
|
|
3709
|
-
mcp:
|
|
4392
|
+
harvest: defaults.harvest,
|
|
4393
|
+
branch: defaults.branch,
|
|
4394
|
+
session: defaults.session,
|
|
4395
|
+
mcp: defaults.mcp,
|
|
3710
4396
|
mcpPrefix: opts.mcpPrefix
|
|
3711
4397
|
});
|
|
3712
4398
|
});
|
|
3713
|
-
program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id
|
|
3714
|
-
"--harvest",
|
|
3715
|
-
"Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
|
|
3716
|
-
).option(
|
|
4399
|
+
program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).option("--preset <name>", "Bundle of model + harvest + branch: fast | smart | max").option("--harvest", "Extract typed plan state from R1 reasoning (Pillar 2)").option(
|
|
3717
4400
|
"--branch <n>",
|
|
3718
4401
|
"Self-consistency: run N parallel samples per turn and pick the most confident",
|
|
3719
4402
|
(v) => Number.parseInt(v, 10)
|
|
3720
4403
|
).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
|
|
3721
4404
|
"--mcp <spec>",
|
|
3722
|
-
'MCP server spec; repeatable. "name=cmd args..."
|
|
4405
|
+
'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE).',
|
|
3723
4406
|
(value, previous = []) => [...previous, value],
|
|
3724
4407
|
[]
|
|
3725
4408
|
).option(
|
|
3726
4409
|
"--mcp-prefix <str>",
|
|
3727
4410
|
"Global prefix (only honored when no per-spec name is set; for a single anonymous server)"
|
|
3728
|
-
).action(async (task, opts) => {
|
|
4411
|
+
).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (task, opts) => {
|
|
4412
|
+
const defaults = resolveDefaults({
|
|
4413
|
+
model: opts.model,
|
|
4414
|
+
harvest: opts.harvest,
|
|
4415
|
+
branch: opts.branch,
|
|
4416
|
+
mcp: opts.mcp,
|
|
4417
|
+
preset: opts.preset,
|
|
4418
|
+
noConfig: opts.config === false
|
|
4419
|
+
});
|
|
3729
4420
|
await runCommand({
|
|
3730
4421
|
task,
|
|
3731
|
-
model:
|
|
4422
|
+
model: defaults.model,
|
|
3732
4423
|
system: opts.system,
|
|
3733
|
-
harvest:
|
|
3734
|
-
branch:
|
|
4424
|
+
harvest: defaults.harvest,
|
|
4425
|
+
branch: defaults.branch,
|
|
3735
4426
|
transcript: opts.transcript,
|
|
3736
|
-
mcp:
|
|
4427
|
+
mcp: defaults.mcp,
|
|
3737
4428
|
mcpPrefix: opts.mcpPrefix
|
|
3738
4429
|
});
|
|
3739
4430
|
});
|