reasonix 0.3.0-alpha.4 → 0.3.1
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 +981 -323
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +114 -3
- package/dist/index.js +326 -193
- 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 {
|
|
@@ -446,6 +684,16 @@ var AppendOnlyLog = class {
|
|
|
446
684
|
extend(messages) {
|
|
447
685
|
for (const m of messages) this.append(m);
|
|
448
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Bulk-replace entries. Intentionally named to be hard to reach for —
|
|
689
|
+
* this is the one mutation path that breaks the log's append-only
|
|
690
|
+
* spirit, reserved for compaction flows (`/compact`) and recovery
|
|
691
|
+
* where the caller has consciously decided to drop old history. Any
|
|
692
|
+
* other use is almost certainly wrong; append() is what you want.
|
|
693
|
+
*/
|
|
694
|
+
compactInPlace(replacement) {
|
|
695
|
+
this._entries = [...replacement];
|
|
696
|
+
}
|
|
449
697
|
get entries() {
|
|
450
698
|
return this._entries;
|
|
451
699
|
}
|
|
@@ -660,74 +908,6 @@ function repairTruncatedJson(input) {
|
|
|
660
908
|
}
|
|
661
909
|
}
|
|
662
910
|
|
|
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
911
|
// src/repair/index.ts
|
|
732
912
|
var ToolCallRepair = class {
|
|
733
913
|
storm;
|
|
@@ -786,21 +966,22 @@ function signature2(call) {
|
|
|
786
966
|
// src/session.ts
|
|
787
967
|
import {
|
|
788
968
|
appendFileSync,
|
|
789
|
-
chmodSync,
|
|
969
|
+
chmodSync as chmodSync2,
|
|
790
970
|
existsSync,
|
|
791
|
-
mkdirSync,
|
|
792
|
-
readFileSync,
|
|
971
|
+
mkdirSync as mkdirSync2,
|
|
972
|
+
readFileSync as readFileSync2,
|
|
793
973
|
readdirSync,
|
|
794
974
|
statSync,
|
|
795
|
-
unlinkSync
|
|
975
|
+
unlinkSync,
|
|
976
|
+
writeFileSync as writeFileSync2
|
|
796
977
|
} from "fs";
|
|
797
|
-
import { homedir } from "os";
|
|
798
|
-
import { dirname, join } from "path";
|
|
978
|
+
import { homedir as homedir2 } from "os";
|
|
979
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
799
980
|
function sessionsDir() {
|
|
800
|
-
return
|
|
981
|
+
return join2(homedir2(), ".reasonix", "sessions");
|
|
801
982
|
}
|
|
802
983
|
function sessionPath(name) {
|
|
803
|
-
return
|
|
984
|
+
return join2(sessionsDir(), `${sanitizeName(name)}.jsonl`);
|
|
804
985
|
}
|
|
805
986
|
function sanitizeName(name) {
|
|
806
987
|
const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
|
|
@@ -810,7 +991,7 @@ function loadSessionMessages(name) {
|
|
|
810
991
|
const path = sessionPath(name);
|
|
811
992
|
if (!existsSync(path)) return [];
|
|
812
993
|
try {
|
|
813
|
-
const raw =
|
|
994
|
+
const raw = readFileSync2(path, "utf8");
|
|
814
995
|
const out = [];
|
|
815
996
|
for (const line of raw.split(/\r?\n/)) {
|
|
816
997
|
const trimmed = line.trim();
|
|
@@ -828,11 +1009,11 @@ function loadSessionMessages(name) {
|
|
|
828
1009
|
}
|
|
829
1010
|
function appendSessionMessage(name, message) {
|
|
830
1011
|
const path = sessionPath(name);
|
|
831
|
-
|
|
1012
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
832
1013
|
appendFileSync(path, `${JSON.stringify(message)}
|
|
833
1014
|
`, "utf8");
|
|
834
1015
|
try {
|
|
835
|
-
|
|
1016
|
+
chmodSync2(path, 384);
|
|
836
1017
|
} catch {
|
|
837
1018
|
}
|
|
838
1019
|
}
|
|
@@ -842,7 +1023,7 @@ function listSessions() {
|
|
|
842
1023
|
try {
|
|
843
1024
|
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
844
1025
|
return files.map((file) => {
|
|
845
|
-
const path =
|
|
1026
|
+
const path = join2(dir, file);
|
|
846
1027
|
const stat = statSync(path);
|
|
847
1028
|
const name = file.replace(/\.jsonl$/, "");
|
|
848
1029
|
const messageCount = countLines(path);
|
|
@@ -861,9 +1042,20 @@ function deleteSession(name) {
|
|
|
861
1042
|
return false;
|
|
862
1043
|
}
|
|
863
1044
|
}
|
|
1045
|
+
function rewriteSession(name, messages) {
|
|
1046
|
+
const path = sessionPath(name);
|
|
1047
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1048
|
+
const body = messages.map((m) => JSON.stringify(m)).join("\n");
|
|
1049
|
+
writeFileSync2(path, body ? `${body}
|
|
1050
|
+
` : "", "utf8");
|
|
1051
|
+
try {
|
|
1052
|
+
chmodSync2(path, 384);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
864
1056
|
function countLines(path) {
|
|
865
1057
|
try {
|
|
866
|
-
const raw =
|
|
1058
|
+
const raw = readFileSync2(path, "utf8");
|
|
867
1059
|
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
868
1060
|
} catch {
|
|
869
1061
|
return 0;
|
|
@@ -876,6 +1068,11 @@ var DEEPSEEK_PRICING = {
|
|
|
876
1068
|
"deepseek-reasoner": { inputCacheHit: 0.14, inputCacheMiss: 0.55, output: 2.19 }
|
|
877
1069
|
};
|
|
878
1070
|
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
1071
|
+
var DEEPSEEK_CONTEXT_TOKENS = {
|
|
1072
|
+
"deepseek-chat": 131072,
|
|
1073
|
+
"deepseek-reasoner": 131072
|
|
1074
|
+
};
|
|
1075
|
+
var DEFAULT_CONTEXT_TOKENS = 131072;
|
|
879
1076
|
function costUsd(model, usage) {
|
|
880
1077
|
const p = DEEPSEEK_PRICING[model];
|
|
881
1078
|
if (!p) return 0;
|
|
@@ -919,12 +1116,14 @@ var SessionStats = class {
|
|
|
919
1116
|
return denom > 0 ? hit / denom : 0;
|
|
920
1117
|
}
|
|
921
1118
|
summary() {
|
|
1119
|
+
const last = this.turns[this.turns.length - 1];
|
|
922
1120
|
return {
|
|
923
1121
|
turns: this.turns.length,
|
|
924
1122
|
totalCostUsd: round(this.totalCost, 6),
|
|
925
1123
|
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
926
1124
|
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
927
|
-
cacheHitRatio: round(this.aggregateCacheHitRatio, 4)
|
|
1125
|
+
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
1126
|
+
lastPromptTokens: last?.usage.promptTokens ?? 0
|
|
928
1127
|
};
|
|
929
1128
|
}
|
|
930
1129
|
};
|
|
@@ -933,81 +1132,6 @@ function round(n, digits) {
|
|
|
933
1132
|
return Math.round(n * f) / f;
|
|
934
1133
|
}
|
|
935
1134
|
|
|
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
1135
|
// src/loop.ts
|
|
1012
1136
|
var CacheFirstLoop = class {
|
|
1013
1137
|
client;
|
|
@@ -1036,7 +1160,7 @@ var CacheFirstLoop = class {
|
|
|
1036
1160
|
this.prefix = opts.prefix;
|
|
1037
1161
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1038
1162
|
this.model = opts.model ?? "deepseek-chat";
|
|
1039
|
-
this.maxToolIters = opts.maxToolIters ??
|
|
1163
|
+
this.maxToolIters = opts.maxToolIters ?? 24;
|
|
1040
1164
|
if (typeof opts.branch === "number") {
|
|
1041
1165
|
this.branchOptions = { budget: opts.branch };
|
|
1042
1166
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1055,12 +1179,49 @@ var CacheFirstLoop = class {
|
|
|
1055
1179
|
this.sessionName = opts.session ?? null;
|
|
1056
1180
|
if (this.sessionName) {
|
|
1057
1181
|
const prior = loadSessionMessages(this.sessionName);
|
|
1058
|
-
|
|
1059
|
-
|
|
1182
|
+
const { messages, healedCount, healedFrom } = healLoadedMessages(
|
|
1183
|
+
prior,
|
|
1184
|
+
DEFAULT_MAX_RESULT_CHARS
|
|
1185
|
+
);
|
|
1186
|
+
for (const msg of messages) this.log.append(msg);
|
|
1187
|
+
this.resumedMessageCount = messages.length;
|
|
1188
|
+
if (healedCount > 0) {
|
|
1189
|
+
process.stderr.write(
|
|
1190
|
+
`\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.
|
|
1191
|
+
`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1060
1194
|
} else {
|
|
1061
1195
|
this.resumedMessageCount = 0;
|
|
1062
1196
|
}
|
|
1063
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Shrink the log by re-truncating oversized tool results to a tighter
|
|
1200
|
+
* cap, and persist the result back to disk so the next launch doesn't
|
|
1201
|
+
* re-inherit a fat session file. Returns a summary the TUI can
|
|
1202
|
+
* display.
|
|
1203
|
+
*
|
|
1204
|
+
* Only tool-role messages are touched (same rationale as
|
|
1205
|
+
* {@link healLoadedMessages}). User and assistant messages carry
|
|
1206
|
+
* authored intent we can't mechanically shrink without losing
|
|
1207
|
+
* meaning.
|
|
1208
|
+
*/
|
|
1209
|
+
compact(tightCapChars = 4e3) {
|
|
1210
|
+
const before = this.log.toMessages();
|
|
1211
|
+
const { messages, healedCount, healedFrom } = healLoadedMessages(before, tightCapChars);
|
|
1212
|
+
const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
1213
|
+
const charsSaved = healedFrom - afterBytes;
|
|
1214
|
+
if (healedCount > 0) {
|
|
1215
|
+
this.log.compactInPlace(messages);
|
|
1216
|
+
if (this.sessionName) {
|
|
1217
|
+
try {
|
|
1218
|
+
rewriteSession(this.sessionName, messages);
|
|
1219
|
+
} catch {
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return { healedCount, charsSaved };
|
|
1224
|
+
}
|
|
1064
1225
|
appendAndPersist(message) {
|
|
1065
1226
|
this.log.append(message);
|
|
1066
1227
|
if (this.sessionName) {
|
|
@@ -1250,7 +1411,7 @@ var CacheFirstLoop = class {
|
|
|
1250
1411
|
turn: this._turn,
|
|
1251
1412
|
role: "error",
|
|
1252
1413
|
content: "",
|
|
1253
|
-
error: err
|
|
1414
|
+
error: formatLoopError(err)
|
|
1254
1415
|
};
|
|
1255
1416
|
return;
|
|
1256
1417
|
}
|
|
@@ -1298,7 +1459,38 @@ var CacheFirstLoop = class {
|
|
|
1298
1459
|
};
|
|
1299
1460
|
}
|
|
1300
1461
|
}
|
|
1301
|
-
yield
|
|
1462
|
+
yield* this.forceSummaryAfterIterLimit();
|
|
1463
|
+
}
|
|
1464
|
+
async *forceSummaryAfterIterLimit() {
|
|
1465
|
+
try {
|
|
1466
|
+
const messages = this.buildMessages(null);
|
|
1467
|
+
const resp = await this.client.chat({
|
|
1468
|
+
model: this.model,
|
|
1469
|
+
messages
|
|
1470
|
+
// no tools → model is forced to answer in text
|
|
1471
|
+
});
|
|
1472
|
+
const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
|
|
1473
|
+
const annotated = `[tool-call budget (${this.maxToolIters}) reached \u2014 forcing summary from what I found]
|
|
1474
|
+
|
|
1475
|
+
${summary}`;
|
|
1476
|
+
const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
|
|
1477
|
+
this.appendAndPersist({ role: "assistant", content: summary });
|
|
1478
|
+
yield {
|
|
1479
|
+
turn: this._turn,
|
|
1480
|
+
role: "assistant_final",
|
|
1481
|
+
content: annotated,
|
|
1482
|
+
stats: summaryStats
|
|
1483
|
+
};
|
|
1484
|
+
yield { turn: this._turn, role: "done", content: summary };
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
yield {
|
|
1487
|
+
turn: this._turn,
|
|
1488
|
+
role: "error",
|
|
1489
|
+
content: "",
|
|
1490
|
+
error: `tool-call budget (${this.maxToolIters}) reached and the fallback summary call failed: ${err.message}. Run /clear and retry with a narrower question, or pass --max-tool-iters higher.`
|
|
1491
|
+
};
|
|
1492
|
+
yield { turn: this._turn, role: "done", content: "" };
|
|
1493
|
+
}
|
|
1302
1494
|
}
|
|
1303
1495
|
async run(userInput, onEvent) {
|
|
1304
1496
|
let final = "";
|
|
@@ -1323,14 +1515,36 @@ function summarizeBranch(chosen, samples) {
|
|
|
1323
1515
|
temperatures: samples.map((s) => s.temperature)
|
|
1324
1516
|
};
|
|
1325
1517
|
}
|
|
1518
|
+
function healLoadedMessages(messages, maxChars) {
|
|
1519
|
+
let healedCount = 0;
|
|
1520
|
+
let healedFrom = 0;
|
|
1521
|
+
const out = messages.map((msg) => {
|
|
1522
|
+
if (msg.role !== "tool") return msg;
|
|
1523
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
1524
|
+
if (content.length <= maxChars) return msg;
|
|
1525
|
+
healedCount += 1;
|
|
1526
|
+
healedFrom += content.length;
|
|
1527
|
+
return { ...msg, content: truncateForModel(content, maxChars) };
|
|
1528
|
+
});
|
|
1529
|
+
return { messages: out, healedCount, healedFrom };
|
|
1530
|
+
}
|
|
1531
|
+
function formatLoopError(err) {
|
|
1532
|
+
const msg = err.message ?? "";
|
|
1533
|
+
if (msg.includes("maximum context length")) {
|
|
1534
|
+
const reqMatch = msg.match(/requested\s+(\d+)\s+tokens/);
|
|
1535
|
+
const requested = reqMatch ? `${Number(reqMatch[1]).toLocaleString()} tokens` : "too many tokens";
|
|
1536
|
+
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.`;
|
|
1537
|
+
}
|
|
1538
|
+
return msg;
|
|
1539
|
+
}
|
|
1326
1540
|
|
|
1327
1541
|
// src/env.ts
|
|
1328
|
-
import { readFileSync as
|
|
1542
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1329
1543
|
import { resolve } from "path";
|
|
1330
1544
|
function loadDotenv(path = ".env") {
|
|
1331
1545
|
let raw;
|
|
1332
1546
|
try {
|
|
1333
|
-
raw =
|
|
1547
|
+
raw = readFileSync3(resolve(process.cwd(), path), "utf8");
|
|
1334
1548
|
} catch {
|
|
1335
1549
|
return;
|
|
1336
1550
|
}
|
|
@@ -1349,7 +1563,7 @@ function loadDotenv(path = ".env") {
|
|
|
1349
1563
|
}
|
|
1350
1564
|
|
|
1351
1565
|
// src/transcript.ts
|
|
1352
|
-
import { createWriteStream, readFileSync as
|
|
1566
|
+
import { createWriteStream, readFileSync as readFileSync4 } from "fs";
|
|
1353
1567
|
function recordFromLoopEvent(ev, extra) {
|
|
1354
1568
|
const rec = {
|
|
1355
1569
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1400,7 +1614,7 @@ function openTranscriptFile(path, meta) {
|
|
|
1400
1614
|
return stream;
|
|
1401
1615
|
}
|
|
1402
1616
|
function readTranscript(path) {
|
|
1403
|
-
const raw =
|
|
1617
|
+
const raw = readFileSync4(path, "utf8");
|
|
1404
1618
|
return parseTranscript(raw);
|
|
1405
1619
|
}
|
|
1406
1620
|
function isPlanStateEmptyShape(s) {
|
|
@@ -1517,12 +1731,14 @@ function summarizeTurns(turns) {
|
|
|
1517
1731
|
}
|
|
1518
1732
|
const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
|
|
1519
1733
|
const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
|
|
1734
|
+
const lastTurn = turns[turns.length - 1];
|
|
1520
1735
|
return {
|
|
1521
1736
|
turns: turns.length,
|
|
1522
1737
|
totalCostUsd: round2(totalCost, 6),
|
|
1523
1738
|
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
1524
1739
|
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
1525
|
-
cacheHitRatio: round2(cacheHitRatio, 4)
|
|
1740
|
+
cacheHitRatio: round2(cacheHitRatio, 4),
|
|
1741
|
+
lastPromptTokens: lastTurn?.usage.promptTokens ?? 0
|
|
1526
1742
|
};
|
|
1527
1743
|
}
|
|
1528
1744
|
function round2(n, digits) {
|
|
@@ -2242,45 +2458,6 @@ var SseTransport = class {
|
|
|
2242
2458
|
}
|
|
2243
2459
|
};
|
|
2244
2460
|
|
|
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
2461
|
// src/mcp/shell-split.ts
|
|
2285
2462
|
function shellSplit(input) {
|
|
2286
2463
|
const tokens = [];
|
|
@@ -2355,51 +2532,8 @@ function parseMcpSpec(input) {
|
|
|
2355
2532
|
return { transport: "stdio", name, command, args };
|
|
2356
2533
|
}
|
|
2357
2534
|
|
|
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
2535
|
// src/index.ts
|
|
2402
|
-
var VERSION = "0.3.
|
|
2536
|
+
var VERSION = "0.3.1";
|
|
2403
2537
|
|
|
2404
2538
|
// src/cli/commands/chat.tsx
|
|
2405
2539
|
import { render } from "ink";
|
|
@@ -2727,7 +2861,15 @@ function StatsPanel({
|
|
|
2727
2861
|
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
2728
2862
|
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
2729
2863
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
2730
|
-
|
|
2864
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
2865
|
+
const ctxRatio = summary.lastPromptTokens / ctxMax;
|
|
2866
|
+
const ctxColor = ctxRatio >= 0.8 ? "red" : ctxRatio >= 0.5 ? "yellow" : void 0;
|
|
2867
|
+
return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cache hit "), /* @__PURE__ */ React5.createElement(Text5, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cost "), /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React5.createElement(Text5, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "saving "), /* @__PURE__ */ React5.createElement(Text5, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%")), summary.lastPromptTokens > 0 ? /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "ctx "), /* @__PURE__ */ React5.createElement(Text5, { color: ctxColor, bold: ctxColor !== void 0 }, formatTokens(summary.lastPromptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " (", (ctxRatio * 100).toFixed(0), "%)"), ctxRatio >= 0.8 ? /* @__PURE__ */ React5.createElement(Text5, { color: "red", bold: true }, " ", "\xB7 /compact") : null) : null));
|
|
2868
|
+
}
|
|
2869
|
+
function formatTokens(n) {
|
|
2870
|
+
if (n < 1e3) return String(n);
|
|
2871
|
+
const k = n / 1e3;
|
|
2872
|
+
return k >= 100 ? `${k.toFixed(0)}k` : `${k.toFixed(1)}k`;
|
|
2731
2873
|
}
|
|
2732
2874
|
|
|
2733
2875
|
// src/cli/ui/slash.ts
|
|
@@ -2738,7 +2880,7 @@ function parseSlash(text) {
|
|
|
2738
2880
|
if (!cmd) return null;
|
|
2739
2881
|
return { cmd, args: parts.slice(1) };
|
|
2740
2882
|
}
|
|
2741
|
-
function handleSlash(cmd, args, loop) {
|
|
2883
|
+
function handleSlash(cmd, args, loop, ctx = {}) {
|
|
2742
2884
|
switch (cmd) {
|
|
2743
2885
|
case "exit":
|
|
2744
2886
|
case "quit":
|
|
@@ -2756,6 +2898,9 @@ function handleSlash(cmd, args, loop) {
|
|
|
2756
2898
|
" /model <id> deepseek-chat or deepseek-reasoner",
|
|
2757
2899
|
" /harvest [on|off] Pillar 2: structured plan-state extraction",
|
|
2758
2900
|
" /branch <N|off> run N parallel samples (N>=2), pick most confident",
|
|
2901
|
+
" /mcp list MCP servers + tools attached to this session",
|
|
2902
|
+
" /setup (exit + reconfigure) \u2192 run `reasonix setup`",
|
|
2903
|
+
" /compact [cap] shrink large tool results in history (default 4k/result)",
|
|
2759
2904
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
2760
2905
|
" /forget delete the current session from disk",
|
|
2761
2906
|
" /clear clear displayed history (log + session kept)",
|
|
@@ -2771,6 +2916,45 @@ function handleSlash(cmd, args, loop) {
|
|
|
2771
2916
|
" reasonix chat --no-session disable persistence for this run"
|
|
2772
2917
|
].join("\n")
|
|
2773
2918
|
};
|
|
2919
|
+
case "mcp": {
|
|
2920
|
+
const specs = ctx.mcpSpecs ?? [];
|
|
2921
|
+
const toolSpecs = loop.prefix.toolSpecs ?? [];
|
|
2922
|
+
if (specs.length === 0 && toolSpecs.length === 0) {
|
|
2923
|
+
return {
|
|
2924
|
+
info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
const lines = [];
|
|
2928
|
+
if (specs.length > 0) {
|
|
2929
|
+
lines.push(`MCP servers (${specs.length}):`);
|
|
2930
|
+
for (const spec of specs) lines.push(` \xB7 ${spec}`);
|
|
2931
|
+
lines.push("");
|
|
2932
|
+
}
|
|
2933
|
+
if (toolSpecs.length > 0) {
|
|
2934
|
+
lines.push(`Tools in registry (${toolSpecs.length}):`);
|
|
2935
|
+
for (const t of toolSpecs) lines.push(` \xB7 ${t.function.name}`);
|
|
2936
|
+
}
|
|
2937
|
+
lines.push("");
|
|
2938
|
+
lines.push("To change this set, exit and run `reasonix setup`.");
|
|
2939
|
+
return { info: lines.join("\n") };
|
|
2940
|
+
}
|
|
2941
|
+
case "setup":
|
|
2942
|
+
return {
|
|
2943
|
+
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
2944
|
+
};
|
|
2945
|
+
case "compact": {
|
|
2946
|
+
const tight = Number.parseInt(args[0] ?? "", 10);
|
|
2947
|
+
const cap = Number.isFinite(tight) && tight >= 500 ? tight : 4e3;
|
|
2948
|
+
const { healedCount, charsSaved } = loop.compact(cap);
|
|
2949
|
+
if (healedCount === 0) {
|
|
2950
|
+
return {
|
|
2951
|
+
info: `\u25B8 nothing to compact \u2014 no tool result in history exceeds ${cap.toLocaleString()} chars.`
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
return {
|
|
2955
|
+
info: `\u25B8 compacted ${healedCount} tool result(s), saved ${charsSaved.toLocaleString()} chars (~${Math.round(charsSaved / 4).toLocaleString()} tokens). Session file rewritten.`
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2774
2958
|
case "sessions": {
|
|
2775
2959
|
const items = listSessions();
|
|
2776
2960
|
if (items.length === 0) {
|
|
@@ -2860,7 +3044,16 @@ function handleSlash(cmd, args, loop) {
|
|
|
2860
3044
|
|
|
2861
3045
|
// src/cli/ui/App.tsx
|
|
2862
3046
|
var FLUSH_INTERVAL_MS = 60;
|
|
2863
|
-
function App({
|
|
3047
|
+
function App({
|
|
3048
|
+
model,
|
|
3049
|
+
system,
|
|
3050
|
+
transcript,
|
|
3051
|
+
harvest: harvest2,
|
|
3052
|
+
branch,
|
|
3053
|
+
session,
|
|
3054
|
+
tools,
|
|
3055
|
+
mcpSpecs
|
|
3056
|
+
}) {
|
|
2864
3057
|
const { exit } = useApp();
|
|
2865
3058
|
const [historical, setHistorical] = useState2([]);
|
|
2866
3059
|
const [streaming, setStreaming] = useState2(null);
|
|
@@ -2871,7 +3064,8 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
|
|
|
2871
3064
|
totalCostUsd: 0,
|
|
2872
3065
|
claudeEquivalentUsd: 0,
|
|
2873
3066
|
savingsVsClaudePct: 0,
|
|
2874
|
-
cacheHitRatio: 0
|
|
3067
|
+
cacheHitRatio: 0,
|
|
3068
|
+
lastPromptTokens: 0
|
|
2875
3069
|
});
|
|
2876
3070
|
const transcriptRef = useRef(null);
|
|
2877
3071
|
if (transcript && !transcriptRef.current) {
|
|
@@ -2948,7 +3142,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
|
|
|
2948
3142
|
setInput("");
|
|
2949
3143
|
const slash = parseSlash(text);
|
|
2950
3144
|
if (slash) {
|
|
2951
|
-
const result = handleSlash(slash.cmd, slash.args, loop);
|
|
3145
|
+
const result = handleSlash(slash.cmd, slash.args, loop, { mcpSpecs });
|
|
2952
3146
|
if (result.exit) {
|
|
2953
3147
|
transcriptRef.current?.end();
|
|
2954
3148
|
exit();
|
|
@@ -3059,7 +3253,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
|
|
|
3059
3253
|
setBusy(false);
|
|
3060
3254
|
}
|
|
3061
3255
|
},
|
|
3062
|
-
[busy, exit, loop, writeTranscript]
|
|
3256
|
+
[busy, exit, loop, mcpSpecs, writeTranscript]
|
|
3063
3257
|
);
|
|
3064
3258
|
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
|
|
3065
3259
|
StatsPanel,
|
|
@@ -3073,7 +3267,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session, to
|
|
|
3073
3267
|
), /* @__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
3268
|
}
|
|
3075
3269
|
function CommandStrip() {
|
|
3076
|
-
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /
|
|
3270
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"));
|
|
3077
3271
|
}
|
|
3078
3272
|
function describeRepair(repair) {
|
|
3079
3273
|
const parts = [];
|
|
@@ -3123,7 +3317,7 @@ function Setup({ onReady }) {
|
|
|
3123
3317
|
}
|
|
3124
3318
|
|
|
3125
3319
|
// src/cli/commands/chat.tsx
|
|
3126
|
-
function Root({ initialKey, tools, ...appProps }) {
|
|
3320
|
+
function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
|
|
3127
3321
|
const [key, setKey] = useState4(initialKey);
|
|
3128
3322
|
if (!key) {
|
|
3129
3323
|
return /* @__PURE__ */ React8.createElement(
|
|
@@ -3146,22 +3340,25 @@ function Root({ initialKey, tools, ...appProps }) {
|
|
|
3146
3340
|
harvest: appProps.harvest,
|
|
3147
3341
|
branch: appProps.branch,
|
|
3148
3342
|
session: appProps.session,
|
|
3149
|
-
tools
|
|
3343
|
+
tools,
|
|
3344
|
+
mcpSpecs
|
|
3150
3345
|
}
|
|
3151
3346
|
);
|
|
3152
3347
|
}
|
|
3153
3348
|
async function chatCommand(opts) {
|
|
3154
3349
|
loadDotenv();
|
|
3155
3350
|
const initialKey = loadApiKey();
|
|
3156
|
-
const
|
|
3351
|
+
const requestedSpecs = opts.mcp ?? [];
|
|
3157
3352
|
const clients = [];
|
|
3353
|
+
const successfulSpecs = [];
|
|
3354
|
+
const failedSpecs = [];
|
|
3158
3355
|
let tools;
|
|
3159
|
-
if (
|
|
3356
|
+
if (requestedSpecs.length > 0) {
|
|
3160
3357
|
tools = new ToolRegistry();
|
|
3161
|
-
for (const raw of
|
|
3358
|
+
for (const raw of requestedSpecs) {
|
|
3162
3359
|
try {
|
|
3163
3360
|
const spec = parseMcpSpec(raw);
|
|
3164
|
-
const prefix = spec.name ? `${spec.name}_` :
|
|
3361
|
+
const prefix = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3165
3362
|
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3166
3363
|
const mcp2 = new McpClient({ transport });
|
|
3167
3364
|
await mcp2.initialize();
|
|
@@ -3173,17 +3370,26 @@ async function chatCommand(opts) {
|
|
|
3173
3370
|
`
|
|
3174
3371
|
);
|
|
3175
3372
|
clients.push(mcp2);
|
|
3373
|
+
successfulSpecs.push(raw);
|
|
3176
3374
|
} catch (err) {
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3375
|
+
const reason = err.message;
|
|
3376
|
+
failedSpecs.push({ spec: raw, reason });
|
|
3377
|
+
process.stderr.write(
|
|
3378
|
+
`\u25B8 MCP setup SKIPPED for "${raw}": ${reason}
|
|
3379
|
+
\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.).
|
|
3380
|
+
`
|
|
3381
|
+
);
|
|
3181
3382
|
}
|
|
3182
3383
|
}
|
|
3384
|
+
if (successfulSpecs.length === 0) {
|
|
3385
|
+
tools = void 0;
|
|
3386
|
+
}
|
|
3183
3387
|
}
|
|
3184
|
-
const
|
|
3185
|
-
|
|
3186
|
-
|
|
3388
|
+
const mcpSpecs = successfulSpecs;
|
|
3389
|
+
const { waitUntilExit } = render(
|
|
3390
|
+
/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, mcpSpecs, ...opts }),
|
|
3391
|
+
{ exitOnCtrlC: true }
|
|
3392
|
+
);
|
|
3187
3393
|
try {
|
|
3188
3394
|
await waitUntilExit();
|
|
3189
3395
|
} finally {
|
|
@@ -3192,7 +3398,7 @@ async function chatCommand(opts) {
|
|
|
3192
3398
|
}
|
|
3193
3399
|
|
|
3194
3400
|
// src/cli/commands/diff.ts
|
|
3195
|
-
import { writeFileSync as
|
|
3401
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
3196
3402
|
import { basename } from "path";
|
|
3197
3403
|
import { render as render2 } from "ink";
|
|
3198
3404
|
import React11 from "react";
|
|
@@ -3338,7 +3544,7 @@ async function diffCommand(opts) {
|
|
|
3338
3544
|
if (wantMarkdown) {
|
|
3339
3545
|
console.log(renderSummaryTable(report));
|
|
3340
3546
|
const md = renderMarkdown(report);
|
|
3341
|
-
|
|
3547
|
+
writeFileSync3(opts.mdPath, md, "utf8");
|
|
3342
3548
|
console.log(`
|
|
3343
3549
|
markdown report written to ${opts.mdPath}`);
|
|
3344
3550
|
return;
|
|
@@ -3362,11 +3568,6 @@ var MCP_CATALOG = [
|
|
|
3362
3568
|
userArgs: "<dir>",
|
|
3363
3569
|
note: "the directory is a hard sandbox \u2014 the server refuses access outside it"
|
|
3364
3570
|
},
|
|
3365
|
-
{
|
|
3366
|
-
name: "fetch",
|
|
3367
|
-
summary: "fetch URLs (markdown-friendly extraction, not a full browser)",
|
|
3368
|
-
package: "@modelcontextprotocol/server-fetch"
|
|
3369
|
-
},
|
|
3370
3571
|
{
|
|
3371
3572
|
name: "memory",
|
|
3372
3573
|
summary: "persistent key-value memory across sessions",
|
|
@@ -3378,12 +3579,6 @@ var MCP_CATALOG = [
|
|
|
3378
3579
|
package: "@modelcontextprotocol/server-github",
|
|
3379
3580
|
note: "set GITHUB_PERSONAL_ACCESS_TOKEN in your env before spawning"
|
|
3380
3581
|
},
|
|
3381
|
-
{
|
|
3382
|
-
name: "sqlite",
|
|
3383
|
-
summary: "read/write a sqlite database file",
|
|
3384
|
-
package: "@modelcontextprotocol/server-sqlite",
|
|
3385
|
-
userArgs: "<db.sqlite>"
|
|
3386
|
-
},
|
|
3387
3582
|
{
|
|
3388
3583
|
name: "puppeteer",
|
|
3389
3584
|
summary: "browser automation \u2014 take screenshots, click, type",
|
|
@@ -3465,7 +3660,9 @@ function ReplayApp({ meta, pages }) {
|
|
|
3465
3660
|
totalCostUsd: cumStats.totalCostUsd,
|
|
3466
3661
|
claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
|
|
3467
3662
|
savingsVsClaudePct: cumStats.savingsVsClaudePct,
|
|
3468
|
-
cacheHitRatio: cumStats.cacheHitRatio
|
|
3663
|
+
cacheHitRatio: cumStats.cacheHitRatio,
|
|
3664
|
+
// Replay is read-only — no live last-turn prompt tokens to show.
|
|
3665
|
+
lastPromptTokens: 0
|
|
3469
3666
|
};
|
|
3470
3667
|
const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
|
|
3471
3668
|
const currentPage = pages[idx];
|
|
@@ -3619,15 +3816,16 @@ async function runCommand(opts) {
|
|
|
3619
3816
|
loadDotenv();
|
|
3620
3817
|
const apiKey = await ensureApiKey();
|
|
3621
3818
|
process.env.DEEPSEEK_API_KEY = apiKey;
|
|
3622
|
-
const
|
|
3819
|
+
const requestedSpecs = opts.mcp ?? [];
|
|
3623
3820
|
const clients = [];
|
|
3624
3821
|
let tools;
|
|
3625
|
-
|
|
3822
|
+
let successCount = 0;
|
|
3823
|
+
if (requestedSpecs.length > 0) {
|
|
3626
3824
|
tools = new ToolRegistry();
|
|
3627
|
-
for (const raw of
|
|
3825
|
+
for (const raw of requestedSpecs) {
|
|
3628
3826
|
try {
|
|
3629
3827
|
const spec = parseMcpSpec(raw);
|
|
3630
|
-
const prefix2 = spec.name ? `${spec.name}_` :
|
|
3828
|
+
const prefix2 = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3631
3829
|
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3632
3830
|
const mcp2 = new McpClient({ transport });
|
|
3633
3831
|
await mcp2.initialize();
|
|
@@ -3638,13 +3836,16 @@ async function runCommand(opts) {
|
|
|
3638
3836
|
`
|
|
3639
3837
|
);
|
|
3640
3838
|
clients.push(mcp2);
|
|
3839
|
+
successCount++;
|
|
3641
3840
|
} catch (err) {
|
|
3642
|
-
process.stderr.write(
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3841
|
+
process.stderr.write(
|
|
3842
|
+
`\u25B8 MCP setup SKIPPED for "${raw}": ${err.message}
|
|
3843
|
+
\u2192 run \`reasonix setup\` to remove broken entries from your saved config.
|
|
3844
|
+
`
|
|
3845
|
+
);
|
|
3646
3846
|
}
|
|
3647
3847
|
}
|
|
3848
|
+
if (successCount === 0) tools = void 0;
|
|
3648
3849
|
}
|
|
3649
3850
|
const client = new DeepSeekClient();
|
|
3650
3851
|
const prefix = new ImmutablePrefix({
|
|
@@ -3784,6 +3985,404 @@ function truncate4(s, max) {
|
|
|
3784
3985
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
|
|
3785
3986
|
}
|
|
3786
3987
|
|
|
3988
|
+
// src/cli/commands/setup.tsx
|
|
3989
|
+
import { render as render4 } from "ink";
|
|
3990
|
+
import React16 from "react";
|
|
3991
|
+
|
|
3992
|
+
// src/cli/ui/Wizard.tsx
|
|
3993
|
+
import { Box as Box12, Text as Text12, useApp as useApp5, useInput as useInput4 } from "ink";
|
|
3994
|
+
import TextInput3 from "ink-text-input";
|
|
3995
|
+
import React15, { useState as useState8 } from "react";
|
|
3996
|
+
|
|
3997
|
+
// src/cli/ui/Select.tsx
|
|
3998
|
+
import { Box as Box11, Text as Text11, useInput as useInput3 } from "ink";
|
|
3999
|
+
import React14, { useState as useState7 } from "react";
|
|
4000
|
+
function SingleSelect({
|
|
4001
|
+
items,
|
|
4002
|
+
initialValue,
|
|
4003
|
+
onSubmit,
|
|
4004
|
+
onCancel
|
|
4005
|
+
}) {
|
|
4006
|
+
const initialIndex = Math.max(
|
|
4007
|
+
0,
|
|
4008
|
+
items.findIndex((i) => i.value === initialValue && !i.disabled)
|
|
4009
|
+
);
|
|
4010
|
+
const [index, setIndex] = useState7(initialIndex === -1 ? 0 : initialIndex);
|
|
4011
|
+
useInput3((_input, key) => {
|
|
4012
|
+
if (key.upArrow) {
|
|
4013
|
+
setIndex((i) => findNextEnabled(items, i, -1));
|
|
4014
|
+
} else if (key.downArrow) {
|
|
4015
|
+
setIndex((i) => findNextEnabled(items, i, 1));
|
|
4016
|
+
} else if (key.return) {
|
|
4017
|
+
const chosen = items[index];
|
|
4018
|
+
if (chosen && !chosen.disabled) onSubmit(chosen.value);
|
|
4019
|
+
} else if (key.escape && onCancel) {
|
|
4020
|
+
onCancel();
|
|
4021
|
+
}
|
|
4022
|
+
});
|
|
4023
|
+
return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => /* @__PURE__ */ React14.createElement(
|
|
4024
|
+
SelectRow,
|
|
4025
|
+
{
|
|
4026
|
+
key: item.value,
|
|
4027
|
+
item,
|
|
4028
|
+
active: i === index,
|
|
4029
|
+
marker: i === index ? "\u25B8" : " "
|
|
4030
|
+
}
|
|
4031
|
+
)));
|
|
4032
|
+
}
|
|
4033
|
+
function MultiSelect({
|
|
4034
|
+
items,
|
|
4035
|
+
initialSelected = [],
|
|
4036
|
+
onSubmit,
|
|
4037
|
+
onCancel,
|
|
4038
|
+
footer
|
|
4039
|
+
}) {
|
|
4040
|
+
const [index, setIndex] = useState7(() => {
|
|
4041
|
+
const first = items.findIndex((i) => !i.disabled);
|
|
4042
|
+
return first === -1 ? 0 : first;
|
|
4043
|
+
});
|
|
4044
|
+
const [selected, setSelected] = useState7(new Set(initialSelected));
|
|
4045
|
+
useInput3((input, key) => {
|
|
4046
|
+
if (key.upArrow) {
|
|
4047
|
+
setIndex((i) => findNextEnabled(items, i, -1));
|
|
4048
|
+
} else if (key.downArrow) {
|
|
4049
|
+
setIndex((i) => findNextEnabled(items, i, 1));
|
|
4050
|
+
} else if (input === " ") {
|
|
4051
|
+
const item = items[index];
|
|
4052
|
+
if (!item || item.disabled) return;
|
|
4053
|
+
setSelected((prev) => {
|
|
4054
|
+
const next = new Set(prev);
|
|
4055
|
+
if (next.has(item.value)) next.delete(item.value);
|
|
4056
|
+
else next.add(item.value);
|
|
4057
|
+
return next;
|
|
4058
|
+
});
|
|
4059
|
+
} else if (key.return) {
|
|
4060
|
+
const ordered = items.filter((i) => selected.has(i.value)).map((i) => i.value);
|
|
4061
|
+
onSubmit(ordered);
|
|
4062
|
+
} else if (key.escape && onCancel) {
|
|
4063
|
+
onCancel();
|
|
4064
|
+
}
|
|
4065
|
+
});
|
|
4066
|
+
return /* @__PURE__ */ React14.createElement(Box11, { flexDirection: "column" }, items.map((item, i) => {
|
|
4067
|
+
const checked = selected.has(item.value);
|
|
4068
|
+
const marker = checked ? "[x]" : "[ ]";
|
|
4069
|
+
return /* @__PURE__ */ React14.createElement(
|
|
4070
|
+
SelectRow,
|
|
4071
|
+
{
|
|
4072
|
+
key: item.value,
|
|
4073
|
+
item,
|
|
4074
|
+
active: i === index,
|
|
4075
|
+
marker: `${i === index ? "\u25B8" : " "} ${marker}`
|
|
4076
|
+
}
|
|
4077
|
+
);
|
|
4078
|
+
}), footer ? /* @__PURE__ */ React14.createElement(Box11, { marginTop: 1 }, /* @__PURE__ */ React14.createElement(Text11, { dimColor: true }, footer)) : null);
|
|
4079
|
+
}
|
|
4080
|
+
function SelectRow({
|
|
4081
|
+
item,
|
|
4082
|
+
active,
|
|
4083
|
+
marker
|
|
4084
|
+
}) {
|
|
4085
|
+
const color = item.disabled ? "gray" : active ? "cyan" : void 0;
|
|
4086
|
+
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);
|
|
4087
|
+
}
|
|
4088
|
+
function findNextEnabled(items, from, step) {
|
|
4089
|
+
if (items.length === 0) return 0;
|
|
4090
|
+
let i = from;
|
|
4091
|
+
for (let tries = 0; tries < items.length; tries++) {
|
|
4092
|
+
i = (i + step + items.length) % items.length;
|
|
4093
|
+
if (!items[i]?.disabled) return i;
|
|
4094
|
+
}
|
|
4095
|
+
return from;
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
// src/cli/ui/presets.ts
|
|
4099
|
+
var PRESETS = {
|
|
4100
|
+
fast: { model: "deepseek-chat", harvest: false, branch: 1 },
|
|
4101
|
+
smart: { model: "deepseek-reasoner", harvest: true, branch: 1 },
|
|
4102
|
+
max: { model: "deepseek-reasoner", harvest: true, branch: 3 }
|
|
4103
|
+
};
|
|
4104
|
+
var PRESET_DESCRIPTIONS = {
|
|
4105
|
+
fast: {
|
|
4106
|
+
headline: "deepseek-chat, no reasoning harvest, no branching",
|
|
4107
|
+
cost: "~1\xA2 per 100 turns \xB7 default"
|
|
4108
|
+
},
|
|
4109
|
+
smart: {
|
|
4110
|
+
headline: "deepseek-reasoner + Pillar 2 harvest",
|
|
4111
|
+
cost: "~10\xD7 cost vs fast \xB7 slower \xB7 better on multi-step tasks"
|
|
4112
|
+
},
|
|
4113
|
+
max: {
|
|
4114
|
+
headline: "reasoner + harvest + self-consistency (3 branches)",
|
|
4115
|
+
cost: "~30\xD7 cost vs fast \xB7 slowest \xB7 for hard single-shots"
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
|
|
4119
|
+
// src/cli/ui/Wizard.tsx
|
|
4120
|
+
var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
|
|
4121
|
+
function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
4122
|
+
const { exit } = useApp5();
|
|
4123
|
+
const [step, setStep] = useState8(existingApiKey ? "preset" : "apiKey");
|
|
4124
|
+
const [data, setData] = useState8({
|
|
4125
|
+
apiKey: existingApiKey ?? "",
|
|
4126
|
+
preset: initial?.preset ?? "fast",
|
|
4127
|
+
selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
|
|
4128
|
+
catalogArgs: {}
|
|
4129
|
+
});
|
|
4130
|
+
const [error, setError] = useState8(null);
|
|
4131
|
+
useInput4((_input, key) => {
|
|
4132
|
+
if (key.escape && step !== "saved" && onCancel) onCancel();
|
|
4133
|
+
});
|
|
4134
|
+
if (step === "apiKey") {
|
|
4135
|
+
return /* @__PURE__ */ React15.createElement(
|
|
4136
|
+
ApiKeyStep,
|
|
4137
|
+
{
|
|
4138
|
+
onSubmit: (key) => {
|
|
4139
|
+
setData((d) => ({ ...d, apiKey: key }));
|
|
4140
|
+
setError(null);
|
|
4141
|
+
setStep("preset");
|
|
4142
|
+
},
|
|
4143
|
+
error,
|
|
4144
|
+
onError: setError
|
|
4145
|
+
}
|
|
4146
|
+
);
|
|
4147
|
+
}
|
|
4148
|
+
if (step === "preset") {
|
|
4149
|
+
return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React15.createElement(
|
|
4150
|
+
SingleSelect,
|
|
4151
|
+
{
|
|
4152
|
+
items: presetItems(),
|
|
4153
|
+
initialValue: data.preset,
|
|
4154
|
+
onSubmit: (preset) => {
|
|
4155
|
+
setData((d) => ({ ...d, preset }));
|
|
4156
|
+
setStep("mcp");
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
), /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\u2191/\u2193 move \xB7 enter confirm \xB7 esc cancel")));
|
|
4160
|
+
}
|
|
4161
|
+
if (step === "mcp") {
|
|
4162
|
+
return /* @__PURE__ */ React15.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React15.createElement(
|
|
4163
|
+
MultiSelect,
|
|
4164
|
+
{
|
|
4165
|
+
items: mcpItems(),
|
|
4166
|
+
initialSelected: data.selectedCatalog,
|
|
4167
|
+
onSubmit: (selected) => {
|
|
4168
|
+
setData((d) => ({ ...d, selectedCatalog: selected }));
|
|
4169
|
+
const needsArgs = selected.some((name) => CATALOG_BY_NAME.get(name)?.userArgs);
|
|
4170
|
+
setStep(needsArgs ? "mcpArgs" : "review");
|
|
4171
|
+
},
|
|
4172
|
+
footer: "\u2191/\u2193 move \xB7 space toggle \xB7 enter confirm \xB7 esc cancel \xB7 leave empty to skip"
|
|
4173
|
+
}
|
|
4174
|
+
));
|
|
4175
|
+
}
|
|
4176
|
+
if (step === "mcpArgs") {
|
|
4177
|
+
const pending = data.selectedCatalog.filter((name) => {
|
|
4178
|
+
const entry2 = CATALOG_BY_NAME.get(name);
|
|
4179
|
+
return entry2?.userArgs && !data.catalogArgs[name];
|
|
4180
|
+
});
|
|
4181
|
+
if (pending.length === 0) {
|
|
4182
|
+
setStep("review");
|
|
4183
|
+
return null;
|
|
4184
|
+
}
|
|
4185
|
+
const currentName = pending[0];
|
|
4186
|
+
const entry = CATALOG_BY_NAME.get(currentName);
|
|
4187
|
+
return /* @__PURE__ */ React15.createElement(
|
|
4188
|
+
McpArgsStep,
|
|
4189
|
+
{
|
|
4190
|
+
entry,
|
|
4191
|
+
error,
|
|
4192
|
+
onSubmit: (value) => {
|
|
4193
|
+
setData((d) => ({
|
|
4194
|
+
...d,
|
|
4195
|
+
catalogArgs: { ...d.catalogArgs, [currentName]: value }
|
|
4196
|
+
}));
|
|
4197
|
+
setError(null);
|
|
4198
|
+
},
|
|
4199
|
+
onError: setError
|
|
4200
|
+
}
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
4203
|
+
if (step === "review") {
|
|
4204
|
+
const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
|
|
4205
|
+
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(
|
|
4206
|
+
SummaryLine,
|
|
4207
|
+
{
|
|
4208
|
+
label: "MCP",
|
|
4209
|
+
value: specs.length === 0 ? "(none)" : `${specs.length} server(s)`
|
|
4210
|
+
}
|
|
4211
|
+
), specs.map((spec, i) => (
|
|
4212
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
|
|
4213
|
+
/* @__PURE__ */ React15.createElement(Box12, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React15.createElement(Text12, { dimColor: true }, "\xB7 ", spec))
|
|
4214
|
+
)), /* @__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(
|
|
4215
|
+
ReviewConfirm,
|
|
4216
|
+
{
|
|
4217
|
+
onConfirm: () => {
|
|
4218
|
+
try {
|
|
4219
|
+
const specsNow = data.selectedCatalog.map(
|
|
4220
|
+
(name) => buildSpec(name, data.catalogArgs)
|
|
4221
|
+
);
|
|
4222
|
+
const prev = readConfig();
|
|
4223
|
+
const next = {
|
|
4224
|
+
...prev,
|
|
4225
|
+
apiKey: data.apiKey,
|
|
4226
|
+
preset: data.preset,
|
|
4227
|
+
mcp: specsNow,
|
|
4228
|
+
setupCompleted: true
|
|
4229
|
+
};
|
|
4230
|
+
writeConfig(next);
|
|
4231
|
+
setStep("saved");
|
|
4232
|
+
onComplete(next);
|
|
4233
|
+
} catch (e) {
|
|
4234
|
+
setError(`Could not save config: ${e.message}`);
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
));
|
|
4239
|
+
}
|
|
4240
|
+
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 }));
|
|
4241
|
+
}
|
|
4242
|
+
function ApiKeyStep({
|
|
4243
|
+
onSubmit,
|
|
4244
|
+
error,
|
|
4245
|
+
onError
|
|
4246
|
+
}) {
|
|
4247
|
+
const [value, setValue] = useState8("");
|
|
4248
|
+
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(
|
|
4249
|
+
TextInput3,
|
|
4250
|
+
{
|
|
4251
|
+
value,
|
|
4252
|
+
onChange: setValue,
|
|
4253
|
+
onSubmit: (raw) => {
|
|
4254
|
+
const trimmed = raw.trim();
|
|
4255
|
+
if (!isPlausibleKey(trimmed)) {
|
|
4256
|
+
onError("Doesn't look like a DeepSeek key. They start with 'sk-' and are 30+ chars.");
|
|
4257
|
+
setValue("");
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
onSubmit(trimmed);
|
|
4261
|
+
},
|
|
4262
|
+
mask: "\u2022",
|
|
4263
|
+
placeholder: "sk-..."
|
|
4264
|
+
}
|
|
4265
|
+
)), 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);
|
|
4266
|
+
}
|
|
4267
|
+
function McpArgsStep({
|
|
4268
|
+
entry,
|
|
4269
|
+
error,
|
|
4270
|
+
onSubmit,
|
|
4271
|
+
onError
|
|
4272
|
+
}) {
|
|
4273
|
+
const [value, setValue] = useState8("");
|
|
4274
|
+
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(
|
|
4275
|
+
TextInput3,
|
|
4276
|
+
{
|
|
4277
|
+
value,
|
|
4278
|
+
onChange: setValue,
|
|
4279
|
+
onSubmit: (raw) => {
|
|
4280
|
+
const trimmed = raw.trim();
|
|
4281
|
+
if (!trimmed) {
|
|
4282
|
+
onError(`${entry.name} needs a value \u2014 got an empty string.`);
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
onSubmit(trimmed);
|
|
4286
|
+
setValue("");
|
|
4287
|
+
},
|
|
4288
|
+
placeholder: placeholderFor(entry)
|
|
4289
|
+
}
|
|
4290
|
+
)), error ? /* @__PURE__ */ React15.createElement(Box12, { marginTop: 1 }, /* @__PURE__ */ React15.createElement(Text12, { color: "red" }, error)) : null));
|
|
4291
|
+
}
|
|
4292
|
+
function ReviewConfirm({ onConfirm }) {
|
|
4293
|
+
useInput4((_i, key) => {
|
|
4294
|
+
if (key.return) onConfirm();
|
|
4295
|
+
});
|
|
4296
|
+
return null;
|
|
4297
|
+
}
|
|
4298
|
+
function ExitOnEnter({ onExit }) {
|
|
4299
|
+
useInput4((_i, key) => {
|
|
4300
|
+
if (key.return) onExit();
|
|
4301
|
+
});
|
|
4302
|
+
return null;
|
|
4303
|
+
}
|
|
4304
|
+
function StepFrame({
|
|
4305
|
+
title,
|
|
4306
|
+
step,
|
|
4307
|
+
total,
|
|
4308
|
+
children
|
|
4309
|
+
}) {
|
|
4310
|
+
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));
|
|
4311
|
+
}
|
|
4312
|
+
function SummaryLine({ label, value }) {
|
|
4313
|
+
return /* @__PURE__ */ React15.createElement(Box12, null, /* @__PURE__ */ React15.createElement(Text12, null, label.padEnd(12)), /* @__PURE__ */ React15.createElement(Text12, { bold: true }, value));
|
|
4314
|
+
}
|
|
4315
|
+
function presetItems() {
|
|
4316
|
+
return ["fast", "smart", "max"].map((name) => ({
|
|
4317
|
+
value: name,
|
|
4318
|
+
label: `${name} \u2014 ${PRESET_DESCRIPTIONS[name].headline}`,
|
|
4319
|
+
hint: PRESET_DESCRIPTIONS[name].cost
|
|
4320
|
+
}));
|
|
4321
|
+
}
|
|
4322
|
+
function mcpItems() {
|
|
4323
|
+
return MCP_CATALOG.map((entry) => {
|
|
4324
|
+
const hintParts = [entry.summary];
|
|
4325
|
+
if (entry.userArgs) hintParts.push(`(you'll provide ${entry.userArgs})`);
|
|
4326
|
+
if (entry.note) hintParts.push(entry.note);
|
|
4327
|
+
return {
|
|
4328
|
+
value: entry.name,
|
|
4329
|
+
label: entry.name,
|
|
4330
|
+
hint: hintParts.join(" \xB7 ")
|
|
4331
|
+
};
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
function placeholderFor(entry) {
|
|
4335
|
+
if (entry.name === "filesystem") return "e.g. /tmp/reasonix-sandbox";
|
|
4336
|
+
if (entry.name === "sqlite") return "e.g. ./notes.sqlite";
|
|
4337
|
+
return entry.userArgs ?? "";
|
|
4338
|
+
}
|
|
4339
|
+
function deriveInitialCatalog(existingSpecs) {
|
|
4340
|
+
const packageToName = new Map(MCP_CATALOG.map((e) => [e.package, e.name]));
|
|
4341
|
+
const out = [];
|
|
4342
|
+
for (const spec of existingSpecs) {
|
|
4343
|
+
for (const [pkg, name] of packageToName) {
|
|
4344
|
+
if (spec.includes(pkg)) {
|
|
4345
|
+
out.push(name);
|
|
4346
|
+
break;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
return out;
|
|
4351
|
+
}
|
|
4352
|
+
function buildSpec(name, argsByName) {
|
|
4353
|
+
const entry = CATALOG_BY_NAME.get(name);
|
|
4354
|
+
if (!entry) return name;
|
|
4355
|
+
const userArg = entry.userArgs ? argsByName[name] : void 0;
|
|
4356
|
+
const tail = userArg ? ` ${quoteIfNeeded(userArg)}` : "";
|
|
4357
|
+
return `${entry.name}=npx -y ${entry.package}${tail}`;
|
|
4358
|
+
}
|
|
4359
|
+
function quoteIfNeeded(s) {
|
|
4360
|
+
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
// src/cli/commands/setup.tsx
|
|
4364
|
+
async function setupCommand(_opts = {}) {
|
|
4365
|
+
loadDotenv();
|
|
4366
|
+
const existingKey = loadApiKey();
|
|
4367
|
+
const existing = readConfig();
|
|
4368
|
+
const { waitUntilExit, unmount } = render4(
|
|
4369
|
+
/* @__PURE__ */ React16.createElement(
|
|
4370
|
+
Wizard,
|
|
4371
|
+
{
|
|
4372
|
+
existingApiKey: existingKey,
|
|
4373
|
+
initial: { preset: existing.preset, mcp: existing.mcp },
|
|
4374
|
+
onComplete: () => {
|
|
4375
|
+
},
|
|
4376
|
+
onCancel: () => {
|
|
4377
|
+
unmount();
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
),
|
|
4381
|
+
{ exitOnCtrlC: true }
|
|
4382
|
+
);
|
|
4383
|
+
await waitUntilExit();
|
|
4384
|
+
}
|
|
4385
|
+
|
|
3787
4386
|
// src/cli/commands/stats.ts
|
|
3788
4387
|
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
3789
4388
|
function statsCommand(opts) {
|
|
@@ -3815,72 +4414,131 @@ function versionCommand() {
|
|
|
3815
4414
|
console.log(`reasonix ${VERSION}`);
|
|
3816
4415
|
}
|
|
3817
4416
|
|
|
4417
|
+
// src/cli/resolve.ts
|
|
4418
|
+
function resolveDefaults(flags) {
|
|
4419
|
+
const cfg = flags.noConfig ? {} : readConfig();
|
|
4420
|
+
const preset = pickPreset(flags.preset, cfg.preset);
|
|
4421
|
+
const presetSettings = PRESETS[preset];
|
|
4422
|
+
const model = flags.model ?? presetSettings.model;
|
|
4423
|
+
const harvest2 = flags.harvest === true ? true : presetSettings.harvest;
|
|
4424
|
+
const branchFromFlag = normalizeBranch(flags.branch);
|
|
4425
|
+
const branch = branchFromFlag ?? (presetSettings.branch > 1 ? presetSettings.branch : void 0);
|
|
4426
|
+
const mcp2 = flags.mcp && flags.mcp.length > 0 ? flags.mcp : cfg.mcp ?? [];
|
|
4427
|
+
const session = resolveSession(flags.session, cfg.session);
|
|
4428
|
+
return { model, harvest: harvest2, branch, mcp: mcp2, session };
|
|
4429
|
+
}
|
|
4430
|
+
function pickPreset(flagPreset, configPreset) {
|
|
4431
|
+
if (flagPreset && isPresetName(flagPreset)) return flagPreset;
|
|
4432
|
+
if (configPreset) return configPreset;
|
|
4433
|
+
return "fast";
|
|
4434
|
+
}
|
|
4435
|
+
function isPresetName(s) {
|
|
4436
|
+
return s === "fast" || s === "smart" || s === "max";
|
|
4437
|
+
}
|
|
4438
|
+
function normalizeBranch(raw) {
|
|
4439
|
+
if (raw === void 0) return void 0;
|
|
4440
|
+
if (!Number.isFinite(raw) || raw <= 1) return void 0;
|
|
4441
|
+
return Math.min(raw, 8);
|
|
4442
|
+
}
|
|
4443
|
+
function resolveSession(flag, configSession) {
|
|
4444
|
+
if (flag === false) return void 0;
|
|
4445
|
+
if (typeof flag === "string" && flag.length > 0) return flag;
|
|
4446
|
+
if (configSession === null) return void 0;
|
|
4447
|
+
if (typeof configSession === "string" && configSession.length > 0) return configSession;
|
|
4448
|
+
return "default";
|
|
4449
|
+
}
|
|
4450
|
+
|
|
3818
4451
|
// src/cli/index.ts
|
|
3819
4452
|
var DEFAULT_SYSTEM = "You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.";
|
|
3820
4453
|
var program = new Command();
|
|
3821
4454
|
program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
|
|
3822
|
-
program.
|
|
4455
|
+
program.action(async () => {
|
|
4456
|
+
const cfg = readConfig();
|
|
4457
|
+
if (!cfg.setupCompleted) {
|
|
4458
|
+
await setupCommand({});
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
const defaults = resolveDefaults({});
|
|
4462
|
+
await chatCommand({
|
|
4463
|
+
model: defaults.model,
|
|
4464
|
+
system: DEFAULT_SYSTEM,
|
|
4465
|
+
harvest: defaults.harvest,
|
|
4466
|
+
branch: defaults.branch,
|
|
4467
|
+
session: defaults.session,
|
|
4468
|
+
mcp: defaults.mcp
|
|
4469
|
+
});
|
|
4470
|
+
});
|
|
4471
|
+
program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
|
|
4472
|
+
await setupCommand({});
|
|
4473
|
+
});
|
|
4474
|
+
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(
|
|
4475
|
+
"--preset <name>",
|
|
4476
|
+
"Bundle of model + harvest + branch. One of: fast, smart, max. Overrides config.preset."
|
|
4477
|
+
).option(
|
|
3823
4478
|
"--harvest",
|
|
3824
|
-
"Extract typed plan state from R1 reasoning (Pillar 2
|
|
4479
|
+
"Extract typed plan state from R1 reasoning (Pillar 2). Overrides preset's harvest setting."
|
|
3825
4480
|
).option(
|
|
3826
4481
|
"--branch <n>",
|
|
3827
4482
|
"Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
|
|
3828
4483
|
(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(
|
|
4484
|
+
).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
4485
|
"--mcp <spec>",
|
|
3834
|
-
'MCP server spec; repeatable.
|
|
4486
|
+
'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE transport). Overrides config.mcp when provided.',
|
|
3835
4487
|
(value, previous = []) => [...previous, value],
|
|
3836
4488
|
[]
|
|
3837
4489
|
).option(
|
|
3838
4490
|
"--mcp-prefix <str>",
|
|
3839
4491
|
"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({
|
|
4492
|
+
).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (opts) => {
|
|
4493
|
+
const defaults = resolveDefaults({
|
|
3850
4494
|
model: opts.model,
|
|
4495
|
+
harvest: opts.harvest,
|
|
4496
|
+
branch: opts.branch,
|
|
4497
|
+
mcp: opts.mcp,
|
|
4498
|
+
session: opts.session,
|
|
4499
|
+
preset: opts.preset,
|
|
4500
|
+
noConfig: opts.config === false
|
|
4501
|
+
});
|
|
4502
|
+
await chatCommand({
|
|
4503
|
+
model: defaults.model,
|
|
3851
4504
|
system: opts.system,
|
|
3852
4505
|
transcript: opts.transcript,
|
|
3853
|
-
harvest:
|
|
3854
|
-
branch:
|
|
3855
|
-
session,
|
|
3856
|
-
mcp:
|
|
4506
|
+
harvest: defaults.harvest,
|
|
4507
|
+
branch: defaults.branch,
|
|
4508
|
+
session: defaults.session,
|
|
4509
|
+
mcp: defaults.mcp,
|
|
3857
4510
|
mcpPrefix: opts.mcpPrefix
|
|
3858
4511
|
});
|
|
3859
4512
|
});
|
|
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(
|
|
4513
|
+
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
4514
|
"--branch <n>",
|
|
3865
4515
|
"Self-consistency: run N parallel samples per turn and pick the most confident",
|
|
3866
4516
|
(v) => Number.parseInt(v, 10)
|
|
3867
4517
|
).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
|
|
3868
4518
|
"--mcp <spec>",
|
|
3869
|
-
'MCP server spec; repeatable. "name=cmd args..."
|
|
4519
|
+
'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE).',
|
|
3870
4520
|
(value, previous = []) => [...previous, value],
|
|
3871
4521
|
[]
|
|
3872
4522
|
).option(
|
|
3873
4523
|
"--mcp-prefix <str>",
|
|
3874
4524
|
"Global prefix (only honored when no per-spec name is set; for a single anonymous server)"
|
|
3875
|
-
).action(async (task, opts) => {
|
|
4525
|
+
).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (task, opts) => {
|
|
4526
|
+
const defaults = resolveDefaults({
|
|
4527
|
+
model: opts.model,
|
|
4528
|
+
harvest: opts.harvest,
|
|
4529
|
+
branch: opts.branch,
|
|
4530
|
+
mcp: opts.mcp,
|
|
4531
|
+
preset: opts.preset,
|
|
4532
|
+
noConfig: opts.config === false
|
|
4533
|
+
});
|
|
3876
4534
|
await runCommand({
|
|
3877
4535
|
task,
|
|
3878
|
-
model:
|
|
4536
|
+
model: defaults.model,
|
|
3879
4537
|
system: opts.system,
|
|
3880
|
-
harvest:
|
|
3881
|
-
branch:
|
|
4538
|
+
harvest: defaults.harvest,
|
|
4539
|
+
branch: defaults.branch,
|
|
3882
4540
|
transcript: opts.transcript,
|
|
3883
|
-
mcp:
|
|
4541
|
+
mcp: defaults.mcp,
|
|
3884
4542
|
mcpPrefix: opts.mcpPrefix
|
|
3885
4543
|
});
|
|
3886
4544
|
});
|