@freesyntax/notch-cli 0.5.22 → 0.5.23

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/index.js CHANGED
@@ -3,15 +3,17 @@ import "./chunk-4HPRBCSY.js";
3
3
  import {
4
4
  MCPClient,
5
5
  buildToolMap,
6
- describeTools,
6
+ describeToolSchemas,
7
+ disconnectMCPServers,
7
8
  drainTailNotifications,
9
+ initMCPServers,
8
10
  listBuiltinAgents,
9
11
  nextSubagentId,
10
12
  parseMCPConfig,
11
13
  pollPendingAgents,
12
14
  setCurrentSurface,
13
15
  spawnSubagent
14
- } from "./chunk-EPSOOCNB.js";
16
+ } from "./chunk-474TAHDN.js";
15
17
  import {
16
18
  Rollout,
17
19
  generateSessionId,
@@ -29,13 +31,12 @@ import "./chunk-O6AKZ4OH.js";
29
31
  import {
30
32
  loadConfig,
31
33
  persistConfigPatch
32
- } from "./chunk-J66N6AFH.js";
34
+ } from "./chunk-UHK6SI4H.js";
33
35
  import "./chunk-KCAR5DOB.js";
34
36
  import {
35
37
  ByokMissingApiKeyError,
36
38
  ByokMissingBaseUrlError,
37
39
  MODEL_CATALOG,
38
- MODEL_IDS,
39
40
  MissingApiKeyError,
40
41
  findByokProvider,
41
42
  isByokRef,
@@ -43,9 +44,11 @@ import {
43
44
  listByokProviders,
44
45
  modelSupportsImages,
45
46
  parseByokRef,
47
+ recordShadowUsage,
46
48
  resolveModel,
47
49
  validateConfig
48
- } from "./chunk-JXQ4HZ47.js";
50
+ } from "./chunk-JVFOAPYV.js";
51
+ import "./chunk-GFVLHUSS.js";
49
52
  import "./chunk-6CZCFY6H.js";
50
53
  import "./chunk-6U3ZAGYA.js";
51
54
  import "./chunk-FFB7GK3Y.js";
@@ -618,6 +621,304 @@ function microcompact(messages, opts2 = {}) {
618
621
  return { messages: out, cleared, savedChars };
619
622
  }
620
623
 
624
+ // src/agent/correction.ts
625
+ var CORRECTION_PRESETS = {
626
+ /** No correction — for capable models (Claude, GPT-4, etc.) */
627
+ disabled: () => ({
628
+ intentGate: false,
629
+ maxCorrectionAttempts: 0,
630
+ useTemplateFormat: false
631
+ }),
632
+ /** Light correction — for mid-tier models (Qwen 14B+, Llama 70B) */
633
+ capable: () => ({
634
+ intentGate: false,
635
+ maxCorrectionAttempts: 1,
636
+ useTemplateFormat: false
637
+ }),
638
+ /** Full correction — for small models (7B-14B) */
639
+ smallModel: () => ({
640
+ intentGate: true,
641
+ maxCorrectionAttempts: 2,
642
+ useTemplateFormat: false
643
+ }),
644
+ /** Maximum scaffolding — for tiny models (sub-7B) */
645
+ tinyModel: () => ({
646
+ intentGate: true,
647
+ maxCorrectionAttempts: 3,
648
+ useTemplateFormat: true
649
+ })
650
+ };
651
+ var INTENT_GATE_PROMPT = `You are a classifier. Given the user's message and the list of available tools, decide: does this message require calling a tool to answer properly?
652
+
653
+ Reply with EXACTLY one word: YES or NO
654
+
655
+ Available tools: {tool_names}
656
+
657
+ User message: {user_message}`;
658
+ async function intentGate(model, userMessage, toolNames) {
659
+ const { generateText: generateText3 } = await import("ai");
660
+ const prompt = INTENT_GATE_PROMPT.replace("{tool_names}", toolNames.join(", ")).replace("{user_message}", userMessage.slice(0, 500));
661
+ try {
662
+ const result = await generateText3({
663
+ model,
664
+ messages: [{ role: "user", content: prompt }],
665
+ maxTokens: 10,
666
+ temperature: 0
667
+ });
668
+ const answer = result.text.trim().toUpperCase();
669
+ if (answer.startsWith("YES")) return "yes";
670
+ if (answer.startsWith("NO")) return "no";
671
+ return "unclear";
672
+ } catch {
673
+ return "unclear";
674
+ }
675
+ }
676
+ function looksLikeFailedToolCall(text, toolNames) {
677
+ if (!text || text.length < 10) return false;
678
+ let weakSignals = 0;
679
+ let strongSignals = 0;
680
+ const lowerText = text.toLowerCase();
681
+ for (const name of toolNames) {
682
+ if (lowerText.includes(name.toLowerCase())) {
683
+ weakSignals++;
684
+ break;
685
+ }
686
+ }
687
+ const jsonPatterns = [
688
+ /\{\s*"name"\s*:/,
689
+ /\{\s*"tool"\s*:/,
690
+ /\{\s*"function"\s*:/,
691
+ /"arguments"\s*:\s*\{/,
692
+ /"parameters"\s*:\s*\{/
693
+ ];
694
+ for (const pat of jsonPatterns) {
695
+ if (pat.test(text)) {
696
+ strongSignals++;
697
+ break;
698
+ }
699
+ }
700
+ const intentPatterns = [
701
+ /\b(?:i'll|let me|i need to|i should|i want to|i will)\s+(?:use|call|invoke|run|execute)\b/i,
702
+ /\b(?:calling|using|invoking|running)\s+(?:the\s+)?(?:tool|function)\b/i
703
+ ];
704
+ for (const pat of intentPatterns) {
705
+ if (pat.test(text)) {
706
+ weakSignals++;
707
+ break;
708
+ }
709
+ }
710
+ const xmlPatterns = [
711
+ /<tool_call/i,
712
+ /<function_call/i,
713
+ /<tool_use/i,
714
+ /<\|tool▁call\|>/,
715
+ /<\|plugin\|>/
716
+ ];
717
+ for (const pat of xmlPatterns) {
718
+ if (pat.test(text)) {
719
+ strongSignals++;
720
+ break;
721
+ }
722
+ }
723
+ if (/```(?:json|tool)?\s*\n?\s*\{/.test(text)) {
724
+ weakSignals++;
725
+ }
726
+ return strongSignals >= 1 || weakSignals >= 2;
727
+ }
728
+ var CORRECTION_PROMPTS = [
729
+ // Attempt 1: XML format (most models have seen this in training)
730
+ `Your previous response tried to call a tool but the format was wrong.
731
+
732
+ Your output was:
733
+ ---
734
+ {raw_output}
735
+ ---
736
+
737
+ To call a tool, you MUST use this exact format:
738
+ <tool_call>
739
+ {"name": "tool_name", "arguments": {"param1": "value1"}}
740
+ </tool_call>
741
+
742
+ Available tools: {tool_names}
743
+
744
+ Rewrite your response with the correct tool call format. Output ONLY the tool call, nothing else.`,
745
+ // Attempt 2: bare JSON (simpler)
746
+ `Your output was not a valid tool call. Rewrite it as a single JSON object:
747
+
748
+ {"name": "TOOL_NAME", "arguments": {"key": "value"}}
749
+
750
+ Available tools: {tool_names}
751
+ Your failed output: {raw_output_short}
752
+
753
+ Reply with ONLY the JSON object.`,
754
+ // Attempt 3: ask directly (simplest possible)
755
+ `Which tool do you want to call? Reply with the tool name and arguments as JSON.
756
+ Tools: {tool_names}
757
+ JSON:`
758
+ ];
759
+ async function correctionPass(model, rawOutput, toolNames, conversationMessages, maxAttempts) {
760
+ const { generateText: generateText3 } = await import("ai");
761
+ for (let i = 0; i < Math.min(maxAttempts, CORRECTION_PROMPTS.length); i++) {
762
+ const promptTemplate = CORRECTION_PROMPTS[i];
763
+ if (!promptTemplate) continue;
764
+ const prompt = promptTemplate.replace("{raw_output}", rawOutput.slice(0, 1500)).replace("{raw_output_short}", rawOutput.slice(0, 500)).replace("{tool_names}", toolNames.join(", "));
765
+ try {
766
+ const result = await generateText3({
767
+ model,
768
+ messages: [
769
+ ...conversationMessages.slice(-4),
770
+ // keep recent context
771
+ { role: "user", content: prompt }
772
+ ],
773
+ maxTokens: 500,
774
+ temperature: 0
775
+ });
776
+ const parsed = extractToolCallsFromText(result.text, toolNames);
777
+ if (parsed.length > 0) return parsed;
778
+ } catch {
779
+ }
780
+ }
781
+ return null;
782
+ }
783
+ function extractTemplateToolCalls(text, toolNames) {
784
+ const calls = [];
785
+ const toolNameSet = new Set(toolNames.map((n) => n.toLowerCase()));
786
+ const lines = text.split("\n");
787
+ let currentTool = null;
788
+ let currentArgs = {};
789
+ for (const line of lines) {
790
+ const trimmed = line.trim();
791
+ const toolMatch = trimmed.match(/^TOOL:\s*(.+)$/i);
792
+ if (toolMatch && toolMatch[1]) {
793
+ if (currentTool && toolNameSet.has(currentTool.toLowerCase())) {
794
+ calls.push({
795
+ id: `correction-${Date.now()}-${calls.length}`,
796
+ name: currentTool,
797
+ args: currentArgs
798
+ });
799
+ }
800
+ currentTool = toolMatch[1].trim();
801
+ currentArgs = {};
802
+ continue;
803
+ }
804
+ const argMatch = trimmed.match(/^ARG_(\w+):\s*(.+)$/i);
805
+ if (argMatch && currentTool && argMatch[1] && argMatch[2]) {
806
+ const key = argMatch[1];
807
+ const rawValue = argMatch[2].trim();
808
+ currentArgs[key] = coerceValue(rawValue);
809
+ }
810
+ }
811
+ if (currentTool && toolNameSet.has(currentTool.toLowerCase())) {
812
+ calls.push({
813
+ id: `correction-${Date.now()}-${calls.length}`,
814
+ name: currentTool,
815
+ args: currentArgs
816
+ });
817
+ }
818
+ return calls;
819
+ }
820
+ function coerceValue(raw) {
821
+ if (raw === "true") return true;
822
+ if (raw === "false") return false;
823
+ if (raw === "null") return null;
824
+ const num = Number(raw);
825
+ if (!Number.isNaN(num) && raw !== "") return num;
826
+ if (raw.startsWith("{") && raw.endsWith("}") || raw.startsWith("[") && raw.endsWith("]")) {
827
+ try {
828
+ return JSON.parse(raw);
829
+ } catch {
830
+ }
831
+ }
832
+ return raw;
833
+ }
834
+ function extractToolCallsFromText(text, toolNames) {
835
+ const toolNameSet = new Set(toolNames.map((n) => n.toLowerCase()));
836
+ const xmlRegex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi;
837
+ let match2;
838
+ const xmlCalls = [];
839
+ while ((match2 = xmlRegex.exec(text)) !== null) {
840
+ const captured = match2[1];
841
+ if (captured) {
842
+ const parsed = tryParseToolJson(captured, toolNameSet);
843
+ if (parsed) xmlCalls.push(parsed);
844
+ }
845
+ }
846
+ if (xmlCalls.length > 0) return xmlCalls;
847
+ const codeBlockRegex = /```(?:json|tool)?\s*\n?([\s\S]*?)\n?\s*```/g;
848
+ const codeCalls = [];
849
+ while ((match2 = codeBlockRegex.exec(text)) !== null) {
850
+ const captured = match2[1];
851
+ if (captured) {
852
+ const parsed = tryParseToolJson(captured, toolNameSet);
853
+ if (parsed) codeCalls.push(parsed);
854
+ }
855
+ }
856
+ if (codeCalls.length > 0) return codeCalls;
857
+ const jsonRegex = /\{[^{}]*"name"\s*:\s*"[^"]+"\s*[,}][\s\S]*?\}/g;
858
+ const jsonCalls = [];
859
+ while ((match2 = jsonRegex.exec(text)) !== null) {
860
+ const parsed = tryParseToolJson(match2[0], toolNameSet);
861
+ if (parsed) jsonCalls.push(parsed);
862
+ }
863
+ if (jsonCalls.length > 0) return jsonCalls;
864
+ const templateCalls = extractTemplateToolCalls(text, toolNames);
865
+ if (templateCalls.length > 0) return templateCalls;
866
+ return [];
867
+ }
868
+ function tryParseToolJson(jsonStr, toolNameSet) {
869
+ try {
870
+ const obj = JSON.parse(jsonStr.trim());
871
+ const name = obj.name ?? obj.tool ?? obj.function;
872
+ if (!name || typeof name !== "string") return null;
873
+ if (!toolNameSet.has(name.toLowerCase())) return null;
874
+ const args = obj.arguments ?? obj.parameters ?? obj.args ?? {};
875
+ return {
876
+ id: `correction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
877
+ name,
878
+ args: typeof args === "object" ? args : {}
879
+ };
880
+ } catch {
881
+ return null;
882
+ }
883
+ }
884
+ var CAPABLE_PATTERNS = [
885
+ /claude/i,
886
+ /gpt-4/i,
887
+ /gpt-3\.5/i,
888
+ /o[1-4]/i,
889
+ /gemini-(?:pro|ultra|2)/i,
890
+ /deepseek-(?:v3|r1)/i
891
+ ];
892
+ var SMALL_PATTERNS = [
893
+ /qwen.*(?:7b|14b|32b)/i,
894
+ /llama.*(?:8b|13b|70b)/i,
895
+ /mistral.*(?:7b|8x7b)/i,
896
+ /gemma.*(?:7b|9b|27b)/i,
897
+ /codestral/i,
898
+ /deepseek-coder/i
899
+ ];
900
+ var TINY_PATTERNS = [
901
+ /qwen.*(?:0\.5b|1\.5b|3b|4b)/i,
902
+ /llama.*(?:1b|3b)/i,
903
+ /gemma.*(?:2b|4b)/i,
904
+ /phi.*(?:1|2|3)/i,
905
+ /tinyllama/i,
906
+ /stablelm/i
907
+ ];
908
+ function autoDetectPreset(modelId) {
909
+ if (!modelId) return CORRECTION_PRESETS.capable();
910
+ for (const pat of CAPABLE_PATTERNS) {
911
+ if (pat.test(modelId)) return CORRECTION_PRESETS.disabled();
912
+ }
913
+ for (const pat of TINY_PATTERNS) {
914
+ if (pat.test(modelId)) return CORRECTION_PRESETS.tinyModel();
915
+ }
916
+ for (const pat of SMALL_PATTERNS) {
917
+ if (pat.test(modelId)) return CORRECTION_PRESETS.smallModel();
918
+ }
919
+ return CORRECTION_PRESETS.capable();
920
+ }
921
+
621
922
  // src/agent/loop.ts
622
923
  function getErrorSignature(toolName, result) {
623
924
  return {
@@ -644,7 +945,18 @@ async function runAgentLoop(messages, config) {
644
945
  let wasCompressed = false;
645
946
  const recentErrors = [];
646
947
  const MAX_REPEATED_ERRORS = 3;
948
+ const correctionCfg = config.correction ?? autoDetectPreset(config.modelId ?? "");
949
+ let totalCorrectionAttempts = 0;
950
+ let intentResult = "unclear";
647
951
  let history = [...messages];
952
+ const toolNames = Object.keys(tools);
953
+ const templateFormatHint = correctionCfg.useTemplateFormat && toolNames.length > 0 ? [
954
+ "",
955
+ "If native tool calling fails, use this fallback format with no extra prose:",
956
+ "TOOL: <tool name>",
957
+ "ARG_<argument_name>: <argument value>",
958
+ `Available tool names: ${toolNames.join(", ")}`
959
+ ].join("\n") : "";
648
960
  await config.toolContext.runHook?.("pre-compact", { messageCount: history.length });
649
961
  history = await autoCompress(history, config.model, contextWindow, () => {
650
962
  wasCompressed = true;
@@ -653,6 +965,13 @@ async function runAgentLoop(messages, config) {
653
965
  if (wasCompressed) {
654
966
  await config.toolContext.runHook?.("post-compact", { messageCount: history.length });
655
967
  }
968
+ if (correctionCfg.intentGate && toolNames.length > 0) {
969
+ const lastUserMsg = [...history].reverse().find((m) => m.role === "user");
970
+ const userText = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
971
+ if (userText) {
972
+ intentResult = await intentGate(config.model, userText, toolNames);
973
+ }
974
+ }
656
975
  while (iterations < maxIter) {
657
976
  iterations++;
658
977
  const mc = microcompact(history, { keepLastN: 6, threshold: 50 });
@@ -666,7 +985,7 @@ async function runAgentLoop(messages, config) {
666
985
  let streamUsage = null;
667
986
  const result = streamText({
668
987
  model: config.model,
669
- system: config.systemPrompt,
988
+ system: `${config.systemPrompt}${templateFormatHint}`,
670
989
  messages: history,
671
990
  tools,
672
991
  maxSteps: 1
@@ -705,8 +1024,67 @@ async function runAgentLoop(messages, config) {
705
1024
  if (streamUsage) {
706
1025
  totalPromptTokens += streamUsage.promptTokens ?? 0;
707
1026
  totalCompletionTokens += streamUsage.completionTokens ?? 0;
1027
+ void recordShadowUsage(
1028
+ config.model,
1029
+ streamUsage
1030
+ );
708
1031
  }
709
1032
  totalToolCalls += toolCalls.length;
1033
+ if (toolCalls.length === 0 && fullText && correctionCfg.maxCorrectionAttempts > 0 && toolNames.length > 0) {
1034
+ const directExtracted = extractToolCallsFromText(fullText, toolNames);
1035
+ const templateExtracted = correctionCfg.useTemplateFormat ? extractTemplateToolCalls(fullText, toolNames) : [];
1036
+ const shouldCorrect = looksLikeFailedToolCall(fullText, toolNames) || directExtracted.length > 0 || templateExtracted.length > 0 || intentResult === "yes" && totalToolCalls === 0;
1037
+ if (shouldCorrect) {
1038
+ const corrected = directExtracted.length > 0 ? directExtracted : templateExtracted.length > 0 ? templateExtracted : await correctionPass(
1039
+ config.model,
1040
+ fullText,
1041
+ toolNames,
1042
+ history,
1043
+ correctionCfg.maxCorrectionAttempts
1044
+ );
1045
+ totalCorrectionAttempts++;
1046
+ if (corrected && corrected.length > 0) {
1047
+ config.onCorrection?.(totalCorrectionAttempts, true);
1048
+ for (const tc of corrected) {
1049
+ toolCalls.push({
1050
+ toolCallId: tc.id,
1051
+ toolName: tc.name,
1052
+ args: tc.args
1053
+ });
1054
+ }
1055
+ totalToolCalls += corrected.length;
1056
+ for (const tc of corrected) {
1057
+ const toolDef = tools[tc.name];
1058
+ if (!toolDef) {
1059
+ toolResults.push({
1060
+ toolCallId: tc.id,
1061
+ result: { content: `Tool "${tc.name}" not found.`, isError: true }
1062
+ });
1063
+ continue;
1064
+ }
1065
+ try {
1066
+ const result2 = await toolDef.execute(tc.args);
1067
+ toolResults.push({ toolCallId: tc.id, result: result2 });
1068
+ config.onToolCall?.(tc.name, tc.args);
1069
+ config.onToolResult?.(
1070
+ tc.name,
1071
+ result2?.content ?? String(result2),
1072
+ result2?.isError ?? false
1073
+ );
1074
+ } catch (err) {
1075
+ const msg = err instanceof Error ? err.message : String(err);
1076
+ toolResults.push({
1077
+ toolCallId: tc.id,
1078
+ result: { content: `Error: ${msg}`, isError: true }
1079
+ });
1080
+ config.onToolResult?.(tc.name, `Error: ${msg}`, true);
1081
+ }
1082
+ }
1083
+ } else {
1084
+ config.onCorrection?.(totalCorrectionAttempts, false);
1085
+ }
1086
+ }
1087
+ }
710
1088
  if (toolCalls.length > 0) {
711
1089
  let hasRepeatedError = false;
712
1090
  for (const tr of toolResults) {
@@ -772,6 +1150,7 @@ async function runAgentLoop(messages, config) {
772
1150
  iterations,
773
1151
  toolCallCount: totalToolCalls,
774
1152
  compressed: wasCompressed,
1153
+ correctionAttempts: totalCorrectionAttempts,
775
1154
  usage: {
776
1155
  promptTokens: totalPromptTokens,
777
1156
  completionTokens: totalCompletionTokens,
@@ -787,6 +1166,7 @@ async function runAgentLoop(messages, config) {
787
1166
  iterations,
788
1167
  toolCallCount: totalToolCalls,
789
1168
  compressed: wasCompressed,
1169
+ correctionAttempts: totalCorrectionAttempts,
790
1170
  usage: {
791
1171
  promptTokens: totalPromptTokens,
792
1172
  completionTokens: totalCompletionTokens,
@@ -794,7 +1174,7 @@ async function runAgentLoop(messages, config) {
794
1174
  }
795
1175
  };
796
1176
  }
797
- async function buildSystemPrompt(projectRoot, _modelId) {
1177
+ async function buildSystemPrompt(projectRoot, _modelId, toolContext) {
798
1178
  const sections = [
799
1179
  // =========================================================
800
1180
  // CACHEABLE PREFIX — stable across turns, sessions, users.
@@ -805,7 +1185,7 @@ async function buildSystemPrompt(projectRoot, _modelId) {
805
1185
  () => [
806
1186
  "You are Notch, an expert AI coding assistant built by Driftrail.",
807
1187
  "You help developers write, debug, refactor, and understand code.",
808
- "You have access to tools for reading/writing files, running shell commands, searching code, and git operations."
1188
+ "You have access to the tools listed in the Tool Schemas section."
809
1189
  ].join("\n")
810
1190
  ),
811
1191
  safeSection(
@@ -841,8 +1221,11 @@ async function buildSystemPrompt(projectRoot, _modelId) {
841
1221
  "- When the user asks for a fix, fix the root cause, not the symptom."
842
1222
  ].join("\n")
843
1223
  ),
844
- safeSection("tools-list", () => `## Available Tools
845
- ${describeTools()}`),
1224
+ DANGEROUS_uncachedSystemPromptSection(
1225
+ "tool-schemas",
1226
+ () => describeToolSchemas(toolContext),
1227
+ "The active tool set depends on coordinator mode, MCP servers, plugins, and permissions for this session."
1228
+ ),
846
1229
  safeSection("builtin-agents", () => {
847
1230
  try {
848
1231
  const agents = listBuiltinAgents();
@@ -1671,8 +2054,7 @@ ${toolContext.cwd}
1671
2054
  ## Repository
1672
2055
  ${repoContext}
1673
2056
 
1674
- ## Available Tools
1675
- ${describeTools()}
2057
+ ${describeToolSchemas(toolContext)}
1676
2058
 
1677
2059
  ## Instructions
1678
2060
  1. Read relevant files to understand the current state
@@ -5403,20 +5785,20 @@ async function ensureGitignoreEntries(p, entries, sectionTitle, ctx) {
5403
5785
  ctx.log(chalk8.green(` Updated .gitignore (+${missing.length})`));
5404
5786
  }
5405
5787
  var DEFAULT_CONFIG = {
5406
- model: "notch-pyre",
5788
+ model: "openrouter/anthropic/claude-sonnet-4-6",
5407
5789
  temperature: 0.3,
5408
5790
  maxIterations: 25,
5409
5791
  useRepoMap: true,
5410
5792
  renderMarkdown: true,
5411
5793
  permissionMode: "auto",
5412
- // BYOK (Bring-Your-Own-Key) passthrough — disabled by default.
5413
- // Uncomment `byok` below (and remove the "_" prefix) to route every
5794
+ // Provider passthrough — disabled by default.
5795
+ // Uncomment `provider` below (and remove the "_" prefix) to route every
5414
5796
  // request to an OpenAI-compatible endpoint (OpenAI / Anthropic /
5415
- // OpenRouter / Together / Fireworks / Groq / Ollama / vLLM / LM Studio
5797
+ // OpenRouter / Google / DeepSeek / Together / Fireworks / Groq / Ollama / vLLM / LM Studio
5416
5798
  // / custom). Providers read their key from the corresponding env var —
5417
5799
  // e.g. OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY.
5418
5800
  // Run `notch --list-providers` to see all built-in ids.
5419
- _byokExample: {
5801
+ _providerExample: {
5420
5802
  provider: "openrouter",
5421
5803
  model: "anthropic/claude-sonnet-4-6"
5422
5804
  }
@@ -7766,8 +8148,7 @@ function buildCompleter(cwd) {
7766
8148
  }
7767
8149
  if (line.startsWith("/model ")) {
7768
8150
  const partial = line.slice(7);
7769
- const modelNames = MODEL_IDS.map((id) => id.replace("notch-", ""));
7770
- const allNames = [...MODEL_IDS, ...modelNames];
8151
+ const allNames = listByokProviders().filter((p) => p.defaultModel).map((p) => `${p.id}/${p.defaultModel}`);
7771
8152
  const matches = allNames.filter((m) => m.startsWith(partial));
7772
8153
  return [matches.map((m) => `/model ${m}`), line];
7773
8154
  }
@@ -7818,10 +8199,9 @@ var SLASH_COMMANDS = [
7818
8199
  { name: "/quit", description: "Exit Notch", category: "Core" },
7819
8200
  // Model & Status
7820
8201
  { name: "/model", description: "Switch or list models", category: "Model" },
7821
- { name: "/model download", description: "Download a Notch model locally", category: "Model" },
7822
- { name: "/downloads", description: "Show local model download progress", category: "Model" },
7823
8202
  { name: "/status", description: "Check API endpoint health", category: "Model" },
7824
- { name: "/sync-keys", description: "Pull BYOK keys from freesyntax.dev", category: "Model" },
8203
+ { name: "/providers", description: "List model providers", category: "Model" },
8204
+ { name: "/sync-keys", description: "Pull provider keys from freesyntax.dev", category: "Model" },
7825
8205
  // Session
7826
8206
  { name: "/save", description: "Save current session", category: "Session" },
7827
8207
  { name: "/sessions", description: "List saved sessions", category: "Session" },
@@ -8352,22 +8732,21 @@ import fs20 from "fs/promises";
8352
8732
  import { createRequire as createRequire2 } from "module";
8353
8733
  var _require2 = createRequire2(import.meta.url);
8354
8734
  var VERSION = _require2("../package.json").version;
8355
- var modelChoices = MODEL_IDS.join(", ");
8356
8735
  if (process.argv[2] === "update") {
8357
8736
  await runUpdateCli(process.argv.slice(3));
8358
8737
  process.exit(process.exitCode ?? 0);
8359
8738
  }
8360
8739
  if (process.argv[2] === "ollama") {
8361
- const { runOllamaCli } = await import("./ollama-launch-P5KBK7AJ.js");
8740
+ const { runOllamaCli } = await import("./ollama-launch-3IKB2A3Z.js");
8362
8741
  const code = await runOllamaCli(process.argv.slice(3), process.cwd());
8363
8742
  process.exit(code);
8364
8743
  }
8365
8744
  if (process.argv[2] === "config") {
8366
- const { runConfigCli } = await import("./config-set-3IWEVZQ4.js");
8745
+ const { runConfigCli } = await import("./config-set-5F4VK7IT.js");
8367
8746
  const code = await runConfigCli(process.argv.slice(3), process.cwd());
8368
8747
  process.exit(code);
8369
8748
  }
8370
- var program = new Command().name("notch").description("Notch CLI \u2014 AI-powered coding assistant by Driftrail").version(VERSION).argument("[prompt...]", "One-shot prompt (runs once and exits)").option(`-m, --model <model>`, `Notch model (${modelChoices}) or BYOK ref like openrouter:anthropic/claude-sonnet-4-6`).option("--base-url <url>", "Override the backend base URL (Notch or BYOK)").option("--api-key <key>", "API key for the backend (prefer the env var: NOTCH_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / OPENROUTER_API_KEY / ...)").option("--provider <id>", "BYOK provider id (openai, anthropic, openrouter, together, fireworks, groq, ollama, lmstudio, vllm, custom). Run --list-providers to see them all.").option("--list-providers", "List built-in BYOK providers and their API-key env vars, then exit").option("--no-repo-map", "Disable automatic repository mapping").option("--no-markdown", "Disable markdown rendering in output").option("--max-iterations <n>", "Max tool-call rounds per turn", "25").option("-y, --yes", "Auto-confirm destructive actions").option("--trust", "Trust mode \u2014 auto-allow all tool calls").option("--theme <theme>", `UI color theme (${THEME_IDS.join(", ")})`).option("--resume", "Resume the last session for this project").option("--session <id>", "Resume a specific session by ID").option("--cwd <dir>", "Set working directory").option("--json", "Emit JSONL event stream on stdout (headless/CI mode)").option("--output-schema <file>", "Path to JSON Schema constraining the final structured output").option("--output-last-message <file>", "Write the final assistant message to this file on exit").option("--guardian", "Enable Guardian: independent Solace-Lite risk scoring before every prompt-level tool call").option("--coordinator", "Coordinator mode: top-level agent can only spawn/continue/stop workers (plus read/grep/glob). All real work is delegated.").option("--no-auto-dream", "Disable the background memory-consolidation daemon (default: enabled in REPL)").option("--no-update", "Disable the background update check on launch (equivalent to NOTCH_AUTO_UPDATE=0)").option("--update-channel <name>", "npm dist-tag to follow for updates (latest | next | beta)").option(
8749
+ var program = new Command().name("notch").description("Notch CLI \u2014 AI-powered coding assistant by Driftrail").version(VERSION).argument("[prompt...]", "One-shot prompt (runs once and exits)").option("-m, --model <model>", "Model id, preferably provider/model (for example openrouter/anthropic/claude-sonnet-4-6)").option("--base-url <url>", "Override the provider base URL (for custom OpenAI-compatible endpoints)").option("--api-key <key>", "API key for the active provider (prefer provider env vars such as OPENROUTER_API_KEY)").option("--provider <id>", "Provider id (openai, anthropic, openrouter, google, deepseek, together, fireworks, groq, ollama, lmstudio, vllm, custom).").option("--list-providers", "List built-in providers and their API-key env vars, then exit").option("--no-repo-map", "Disable automatic repository mapping").option("--no-markdown", "Disable markdown rendering in output").option("--max-iterations <n>", "Max tool-call rounds per turn", "25").option("-y, --yes", "Auto-confirm destructive actions").option("--trust", "Trust mode \u2014 auto-allow all tool calls").option("--theme <theme>", `UI color theme (${THEME_IDS.join(", ")})`).option("--resume", "Resume the last session for this project").option("--session <id>", "Resume a specific session by ID").option("--cwd <dir>", "Set working directory").option("--json", "Emit JSONL event stream on stdout (headless/CI mode)").option("--output-schema <file>", "Path to JSON Schema constraining the final structured output").option("--output-last-message <file>", "Write the final assistant message to this file on exit").option("--guardian", "Enable Guardian risk scoring when a provider-backed guardian model is configured").option("--coordinator", "Coordinator mode: top-level agent can only spawn/continue/stop workers (plus read/grep/glob). All real work is delegated.").option("--no-auto-dream", "Disable the background memory-consolidation daemon (default: enabled in REPL)").option("--no-update", "Disable the background update check on launch (equivalent to NOTCH_AUTO_UPDATE=0)").option("--update-channel <name>", "npm dist-tag to follow for updates (latest | next | beta)").option(
8371
8750
  "--image <path>",
8372
8751
  "Attach an image (file path, URL, or data URL). Repeatable.",
8373
8752
  (val, prev) => prev ? [...prev, val] : [val],
@@ -8377,22 +8756,29 @@ var opts = program.opts();
8377
8756
  var promptArgs = program.args;
8378
8757
  function printByokProviderList() {
8379
8758
  const t = theme();
8380
- console.log(t.dim("\n BYOK providers (point --provider at any of these):\n"));
8381
- const header = ` ${"id".padEnd(12)} ${"label".padEnd(20)} ${"env var".padEnd(22)} default model`;
8759
+ console.log(t.dim("\n Model providers (point --provider at any of these):\n"));
8760
+ const providers = listByokProviders();
8761
+ const idWidth = Math.max(12, ...providers.map((p) => p.id.length));
8762
+ const labelWidth = Math.max(20, ...providers.map((p) => p.label.length));
8763
+ const envWidth = Math.max(22, ...providers.map((p) => (p.apiKeyEnv || "(none)").length));
8764
+ const keyWidth = 7;
8765
+ const header = ` ${"id".padEnd(idWidth)} ${"label".padEnd(labelWidth)} ${"env var".padEnd(envWidth)} ${"key".padEnd(keyWidth)} default model`;
8382
8766
  console.log(t.dim(header));
8383
8767
  console.log(t.dim(` ${"-".repeat(header.length - 2)}`));
8384
- for (const p of listByokProviders()) {
8385
- const keyPresent = p.apiKeyEnv ? process.env[p.apiKeyEnv] ? t.success("\u2713") : t.dim("\u2717") : t.dim("\u2013");
8768
+ for (const p of providers) {
8769
+ const keyLabel = p.apiKeyEnv ? process.env[p.apiKeyEnv] ? "set" : p.fallbackApiKey ? "local" : "missing" : "none";
8770
+ const paddedKey = keyLabel.padEnd(keyWidth);
8771
+ const keyPresent = keyLabel === "set" ? t.success(paddedKey) : keyLabel === "missing" ? t.dim(paddedKey) : t.dim(paddedKey);
8386
8772
  const envDisplay = p.apiKeyEnv || "(none)";
8387
8773
  console.log(
8388
- ` ${t.brand(p.id.padEnd(12))} ${p.label.padEnd(20)} ${envDisplay.padEnd(20)} ${keyPresent} ${t.dim(p.defaultModel || "\u2014")}`
8774
+ ` ${t.brand(p.id.padEnd(idWidth))} ${p.label.padEnd(labelWidth)} ${envDisplay.padEnd(envWidth)} ${keyPresent} ${t.dim(p.defaultModel || "\u2014")}`
8389
8775
  );
8390
8776
  }
8391
8777
  console.log("");
8392
8778
  console.log(t.dim(" Use it like:"));
8393
8779
  console.log(t.dim(" export OPENROUTER_API_KEY=sk-or-..."));
8394
- console.log(t.dim(" notch --provider openrouter --model anthropic/claude-opus-4-7"));
8395
- console.log(t.dim(" notch --model openrouter:anthropic/claude-opus-4-7 # equivalent"));
8780
+ console.log(t.dim(" notch --provider openrouter --model anthropic/claude-sonnet-4-6"));
8781
+ console.log(t.dim(" notch --model openrouter/anthropic/claude-sonnet-4-6 # equivalent"));
8396
8782
  console.log(t.dim(" notch --provider custom --base-url http://localhost:8000/v1 --model my-model"));
8397
8783
  console.log("");
8398
8784
  }
@@ -8414,7 +8800,8 @@ async function persistByokChoice(projectRoot, providerId, defaultModel) {
8414
8800
  } catch {
8415
8801
  }
8416
8802
  const idToPersist = providerId === "__custom__" ? "custom" : providerId;
8417
- current.byok = { provider: idToPersist, model: defaultModel };
8803
+ current.provider = { provider: idToPersist, model: defaultModel };
8804
+ delete current.byok;
8418
8805
  await fs20.writeFile(p, JSON.stringify(current, null, 2) + "\n", "utf-8");
8419
8806
  } catch {
8420
8807
  }
@@ -8428,14 +8815,12 @@ function interactiveModelPicker(activeModel) {
8428
8815
  return new Promise((resolve3) => {
8429
8816
  const t = theme();
8430
8817
  const rows = [];
8431
- rows.push({ kind: "notch-header" });
8432
- for (const id of MODEL_IDS) rows.push({ kind: "notch", id });
8433
8818
  rows.push({ kind: "byok-header" });
8434
8819
  for (const p of listByokProviders()) rows.push({ kind: "byok", provider: p });
8435
- const selectableIndexes = rows.map((r, i) => r.kind === "notch" || r.kind === "byok" ? i : -1).filter((i) => i >= 0);
8820
+ const selectableIndexes = rows.map((r, i) => r.kind === "byok" ? i : -1).filter((i) => i >= 0);
8436
8821
  let cursor = selectableIndexes.find((i) => {
8437
8822
  const r = rows[i];
8438
- return r && r.kind === "notch" && r.id === activeModel;
8823
+ return r && r.kind === "byok" && typeof activeModel === "string" && isByokRef(activeModel) && parseByokRef(activeModel).provider === r.provider.id;
8439
8824
  }) ?? selectableIndexes[0] ?? 0;
8440
8825
  const rowCount = rows.length;
8441
8826
  const headerLines = 2;
@@ -8452,46 +8837,31 @@ function interactiveModelPicker(activeModel) {
8452
8837
  console.log(t.dim(" Select a model (\u2191\u2193 to move, Enter to select, Esc to cancel)\n"));
8453
8838
  for (let i = 0; i < rows.length; i++) {
8454
8839
  const row = rows[i];
8455
- if (row.kind === "notch-header") {
8456
- console.log(` ${t.dim("\u2500\u2500\u2500 Notch models (default) \u2500\u2500\u2500")}`);
8457
- continue;
8458
- }
8459
8840
  if (row.kind === "byok-header") {
8460
- console.log(` ${t.dim("\u2500\u2500\u2500 BYOK providers (your own API key) \u2500\u2500\u2500")}`);
8841
+ console.log(` ${t.dim("--- Providers ---")}`);
8461
8842
  continue;
8462
8843
  }
8463
8844
  const isSelected = i === cursor;
8464
8845
  const pointer = isSelected ? t.brand("\u276F") : " ";
8465
- if (row.kind === "notch") {
8466
- const info = MODEL_CATALOG[row.id];
8467
- const isCurrent = row.id === activeModel;
8468
- const dot = isCurrent ? t.success("\u25CF") : " ";
8469
- const label = isSelected ? t.bold(info.label) : t.dim(info.label);
8470
- const size = t.dim(info.size);
8471
- const gpu = t.dim(info.gpu);
8472
- const ctx = t.dim(`${(info.contextWindow / 1024).toFixed(0)}K`);
8473
- console.log(` ${pointer} ${dot} ${t.brand(row.id.replace("notch-", "").padEnd(12))} ${label.padEnd(20)} ${size.padEnd(6)} ${gpu.padEnd(12)} ${ctx}`);
8474
- } else {
8475
- const p = row.provider;
8476
- const isCurrent = typeof activeModel === "string" && isByokRef(activeModel) ? parseByokRef(activeModel).provider === p.id : false;
8477
- const dot = isCurrent ? t.success("\u25CF") : " ";
8478
- const envHit = p.apiKeyEnv ? Boolean(process.env[p.apiKeyEnv]) : false;
8479
- let syncHit = false;
8480
- if (!envHit) {
8481
- try {
8482
- const { loadSyncedByokKeysSync } = (init_auth(), __toCommonJS(auth_exports));
8483
- const synced = loadSyncedByokKeysSync();
8484
- const fromSync = synced?.keys[p.id];
8485
- syncHit = typeof fromSync === "string" && fromSync.length > 0;
8486
- } catch {
8487
- }
8846
+ const p = row.provider;
8847
+ const isCurrent = typeof activeModel === "string" && isByokRef(activeModel) ? parseByokRef(activeModel).provider === p.id : false;
8848
+ const dot = isCurrent ? t.success("\u25CF") : " ";
8849
+ const envHit = p.apiKeyEnv ? Boolean(process.env[p.apiKeyEnv]) : false;
8850
+ let syncHit = false;
8851
+ if (!envHit) {
8852
+ try {
8853
+ const { loadSyncedByokKeysSync } = (init_auth(), __toCommonJS(auth_exports));
8854
+ const synced = loadSyncedByokKeysSync();
8855
+ const fromSync = synced?.keys[p.id];
8856
+ syncHit = typeof fromSync === "string" && fromSync.length > 0;
8857
+ } catch {
8488
8858
  }
8489
- const keyPresent = p.apiKeyEnv ? envHit ? t.success("\u2713") : syncHit ? t.brand("\u2713") : t.dim("\u2717") : t.dim("\u2013");
8490
- const label = isSelected ? t.bold(p.label) : t.dim(p.label);
8491
- const envDisplay = t.dim((p.apiKeyEnv || "local").padEnd(22));
8492
- const defModel = t.dim(p.defaultModel ? p.defaultModel.slice(0, 34) : "\u2014");
8493
- console.log(` ${pointer} ${dot} ${t.brand(p.id.padEnd(12))} ${label.padEnd(20)} ${envDisplay} ${keyPresent} ${defModel}`);
8494
8859
  }
8860
+ const keyPresent = p.apiKeyEnv ? envHit ? t.success("\u2713") : syncHit ? t.brand("\u2713") : t.dim("\u2717") : t.dim("\u2013");
8861
+ const label = isSelected ? t.bold(p.label) : t.dim(p.label);
8862
+ const envDisplay = t.dim((p.apiKeyEnv || "local").padEnd(22));
8863
+ const defModel = t.dim(p.defaultModel ? p.defaultModel.slice(0, 34) : "\u2014");
8864
+ console.log(` ${pointer} ${dot} ${t.brand(p.id.padEnd(12))} ${label.padEnd(20)} ${envDisplay} ${keyPresent} ${defModel}`);
8495
8865
  }
8496
8866
  };
8497
8867
  const render = (first) => {
@@ -8517,16 +8887,12 @@ function interactiveModelPicker(activeModel) {
8517
8887
  } else if (s === "\r" || s === "\n") {
8518
8888
  const row = rows[cursor];
8519
8889
  cleanup();
8520
- if (!row || row.kind !== "notch" && row.kind !== "byok") {
8890
+ if (!row || row.kind !== "byok") {
8521
8891
  resolve3(null);
8522
8892
  return;
8523
8893
  }
8524
- if (row.kind === "notch") {
8525
- resolve3({ kind: "notch", id: row.id });
8526
- } else {
8527
- const modelRef = `${row.provider.id}:${row.provider.defaultModel}`;
8528
- resolve3({ kind: "byok", provider: row.provider, modelRef });
8529
- }
8894
+ const modelRef = `${row.provider.id}/${row.provider.defaultModel}`;
8895
+ resolve3({ kind: "byok", provider: row.provider, modelRef });
8530
8896
  } else if (s === "\x1B" || s === "") {
8531
8897
  cleanup();
8532
8898
  resolve3(null);
@@ -8543,13 +8909,11 @@ function interactiveModelPicker(activeModel) {
8543
8909
  function printHelp() {
8544
8910
  console.log(chalk30.gray(`
8545
8911
  Commands:
8546
- /model \u2014 Show available models (Notch + BYOK)
8547
- /model <name> \u2014 Switch model: /model pyre OR /model openrouter:anthropic/claude-sonnet-4-6
8548
- /model download <name> \u2014 Pull a Notch model's local weights in the background (HF Hub)
8549
- /downloads \u2014 Show local download progress for this session
8550
- /sync-keys \u2014 Pull BYOK keys you added on freesyntax.dev
8551
- /providers \u2014 List built-in BYOK providers and whether their keys are set
8552
- /status \u2014 Check backend health (Notch API or BYOK endpoint)
8912
+ /model \u2014 Pick a provider and default model
8913
+ /model <ref> \u2014 Switch model: /model openrouter/anthropic/claude-sonnet-4-6
8914
+ /sync-keys \u2014 Pull provider keys you added on freesyntax.dev
8915
+ /providers \u2014 List built-in providers and whether their keys are set
8916
+ /status \u2014 Check active provider health
8553
8917
  /undo \u2014 Undo last file changes
8554
8918
  /usage \u2014 Show token usage + context meter
8555
8919
  /cost \u2014 Show estimated session cost
@@ -8656,16 +9020,16 @@ async function main() {
8656
9020
  const providerCount = Object.keys(synced.keys).length;
8657
9021
  if (providerCount > 0) {
8658
9022
  console.log(chalk30.gray(
8659
- ` Synced ${providerCount} BYOK provider key(s) from freesyntax.dev \u2192 ${(await import("./auth-UAMMP5IJ.js")).getByokSyncPath()}`
9023
+ ` Synced ${providerCount} provider key(s) from freesyntax.dev \u2192 ${(await import("./auth-UAMMP5IJ.js")).getByokSyncPath()}`
8660
9024
  ));
8661
9025
  } else {
8662
9026
  console.log(chalk30.gray(
8663
- ` No BYOK keys on your profile yet \u2014 add them at freesyntax.dev/settings/keys then run ${chalk30.white("notch sync-keys")}.`
9027
+ ` No provider keys on your profile yet \u2014 add them at freesyntax.dev/settings/keys then run ${chalk30.white("notch sync-keys")}.`
8664
9028
  ));
8665
9029
  }
8666
9030
  } catch (syncErr) {
8667
9031
  console.log(chalk30.yellow(
8668
- ` (BYOK sync skipped: ${syncErr.message.slice(0, 120)})`
9032
+ ` (provider key sync skipped: ${syncErr.message.slice(0, 120)})`
8669
9033
  ));
8670
9034
  }
8671
9035
  console.log("");
@@ -8684,7 +9048,7 @@ async function main() {
8684
9048
  console.log(chalk30.gray("\n Not signed in. Run: notch login\n"));
8685
9049
  return;
8686
9050
  }
8687
- const spinner = ora7("Pulling BYOK keys from freesyntax.dev...").start();
9051
+ const spinner = ora7("Pulling provider keys from freesyntax.dev...").start();
8688
9052
  try {
8689
9053
  const { syncByokKeys, getByokSyncPath } = await import("./auth-UAMMP5IJ.js");
8690
9054
  const synced = await syncByokKeys(creds.token);
@@ -8692,7 +9056,7 @@ async function main() {
8692
9056
  const providers = Object.keys(synced.keys);
8693
9057
  if (providers.length === 0) {
8694
9058
  console.log(chalk30.gray(`
8695
- No BYOK keys on your profile yet.`));
9059
+ No provider keys on your profile yet.`));
8696
9060
  console.log(chalk30.gray(` Add them at ${chalk30.white("https://freesyntax.dev/settings/keys")} then rerun.
8697
9061
  `));
8698
9062
  } else {
@@ -8737,7 +9101,7 @@ async function main() {
8737
9101
  return;
8738
9102
  }
8739
9103
  if (promptArgs[0] === "mcp-serve" || promptArgs[0] === "mcp-server") {
8740
- const { runMcpServer } = await import("./server-IGOZHW52.js");
9104
+ const { runMcpServer } = await import("./server-GMF4WV67.js");
8741
9105
  await runMcpServer({
8742
9106
  cwd: opts.cwd ?? process.cwd(),
8743
9107
  version: VERSION,
@@ -8776,7 +9140,7 @@ async function main() {
8776
9140
  const providerId = opts.provider === "custom" ? "__custom__" : opts.provider;
8777
9141
  const byokInfo = findByokProvider(providerId);
8778
9142
  if (!byokInfo) {
8779
- console.error(chalk30.red(` Unknown BYOK provider: ${opts.provider}`));
9143
+ console.error(chalk30.red(` Unknown provider: ${opts.provider}`));
8780
9144
  console.error(chalk30.gray(` Run notch --list-providers to see built-ins, or pass --provider custom --base-url <url>.`));
8781
9145
  process.exit(1);
8782
9146
  }
@@ -8793,7 +9157,7 @@ async function main() {
8793
9157
  } else if (isByokRef(opts.model)) {
8794
9158
  const { provider } = parseByokRef(opts.model);
8795
9159
  if (!findByokProvider(provider)) {
8796
- console.error(chalk30.red(` Unknown BYOK provider in ref: ${provider}`));
9160
+ console.error(chalk30.red(` Unknown provider in model ref: ${provider}`));
8797
9161
  console.error(chalk30.gray(` Run notch --list-providers to see built-ins.`));
8798
9162
  process.exit(1);
8799
9163
  }
@@ -8801,8 +9165,8 @@ async function main() {
8801
9165
  config.models.chat.byokProvider = void 0;
8802
9166
  } else {
8803
9167
  console.error(chalk30.red(` Unknown model: ${opts.model}`));
8804
- console.error(chalk30.gray(` Notch models: ${modelChoices}`));
8805
- console.error(chalk30.gray(` For BYOK use "<provider>:<model>" (e.g. openrouter:anthropic/claude-sonnet-4-6), or run notch --list-providers .`));
9168
+ console.error(chalk30.gray(" Use provider/model, for example openrouter/anthropic/claude-sonnet-4-6."));
9169
+ console.error(chalk30.gray(" Run notch --list-providers to see built-ins."));
8806
9170
  process.exit(1);
8807
9171
  }
8808
9172
  }
@@ -8856,7 +9220,7 @@ async function main() {
8856
9220
  if (err instanceof ByokMissingApiKeyError) {
8857
9221
  printWordmark(VERSION);
8858
9222
  const p = err.provider;
8859
- console.log(` To use ${p.label} (BYOK) you need an API key.`);
9223
+ console.log(` To use ${p.label} you need an API key.`);
8860
9224
  console.log("");
8861
9225
  if (p.apiKeyEnv) {
8862
9226
  console.log(" \x1B[1mOption 1:\x1B[0m Set the env var");
@@ -8883,9 +9247,9 @@ async function main() {
8883
9247
  console.log(" \x1B[1mOption 3:\x1B[0m Pass it inline");
8884
9248
  console.log(" \x1B[33m$ notch --api-key your-key-here\x1B[0m");
8885
9249
  console.log("");
8886
- console.log(" \x1B[1mOr:\x1B[0m Bring your own key (OpenAI, Anthropic, OpenRouter, ...)");
9250
+ console.log(" \x1B[1mOr:\x1B[0m Use any provider key (OpenAI, Anthropic, OpenRouter, ...)");
8887
9251
  console.log(" \x1B[33m$ notch --list-providers\x1B[0m");
8888
- console.log(" \x1B[33m$ notch --provider openrouter --model anthropic/claude-opus-4-7\x1B[0m");
9252
+ console.log(" \x1B[33m$ notch --model openrouter/anthropic/claude-sonnet-4-6\x1B[0m");
8889
9253
  console.log("");
8890
9254
  console.log(" Get your Notch key at: \x1B[4mhttps://freesyntax.dev/settings\x1B[0m");
8891
9255
  console.log("");
@@ -8896,7 +9260,7 @@ async function main() {
8896
9260
  const info = activeByok ? {
8897
9261
  id: activeModelId,
8898
9262
  label: `${activeByok.label}`,
8899
- size: "BYOK",
9263
+ size: "provider",
8900
9264
  gpu: activeByok.id,
8901
9265
  contextWindow: 128e3,
8902
9266
  maxOutputTokens: 8192,
@@ -8976,7 +9340,6 @@ async function main() {
8976
9340
  spinner.warn("Could not build repo map");
8977
9341
  }
8978
9342
  }
8979
- const baseSystemPrompt = await buildSystemPrompt(config.projectRoot, activeModelId);
8980
9343
  let outputSchema = null;
8981
9344
  if (opts.outputSchema) {
8982
9345
  try {
@@ -8991,19 +9354,6 @@ async function main() {
8991
9354
  }
8992
9355
  }
8993
9356
  const coordinatorMode = !!opts.coordinator || isCoordinatorModeEnv();
8994
- const systemPrompt = coordinatorMode ? [
8995
- COORDINATOR_SYSTEM_PROMPT,
8996
- repoMapStr ? `
8997
- ## Repository Map
8998
- ${repoMapStr}` : "",
8999
- outputSchema ? schemaInstructions(outputSchema) : ""
9000
- ].join("") : [
9001
- baseSystemPrompt,
9002
- repoMapStr ? `
9003
- ## Repository Map
9004
- ${repoMapStr}` : "",
9005
- outputSchema ? schemaInstructions(outputSchema) : ""
9006
- ].join("");
9007
9357
  if (coordinatorMode && !jsonMode) {
9008
9358
  console.log(
9009
9359
  chalk30.green(
@@ -9029,19 +9379,11 @@ ${repoMapStr}` : "",
9029
9379
  const branches = /* @__PURE__ */ new Map();
9030
9380
  let currentBranch = "main";
9031
9381
  const costTracker = new CostTracker();
9032
- const mcpClients = [];
9033
9382
  try {
9034
9383
  const configRaw = await fs20.readFile(nodePath2.resolve(config.projectRoot, ".notch.json"), "utf-8").catch(() => "{}");
9035
- const mcpConfigs = parseMCPConfig(JSON.parse(configRaw));
9036
- for (const [name, mcpConfig] of Object.entries(mcpConfigs)) {
9037
- try {
9038
- const client = new MCPClient(mcpConfig, name);
9039
- await client.connect();
9040
- mcpClients.push(client);
9041
- console.log(chalk30.green(` MCP: Connected to ${name} (${client.tools.length} tools)`));
9042
- } catch (err) {
9043
- console.log(chalk30.yellow(` MCP: Could not connect to ${name}: ${err.message}`));
9044
- }
9384
+ const toolCount = await initMCPServers(JSON.parse(configRaw));
9385
+ if (toolCount > 0) {
9386
+ console.log(chalk30.green(` MCP: Connected (${toolCount} tools)`));
9045
9387
  }
9046
9388
  } catch {
9047
9389
  }
@@ -9059,13 +9401,16 @@ ${repoMapStr}` : "",
9059
9401
  if (opts.guardian) {
9060
9402
  try {
9061
9403
  guardianModel = resolveModel({
9062
- model: "notch-solace-lite",
9404
+ model: process.env.NOTCH_GUARDIAN_MODEL ?? config.models.chat.model,
9063
9405
  apiKey: config.models.chat.apiKey,
9064
9406
  baseUrl: process.env.NOTCH_GUARDIAN_BASE_URL,
9065
- headers: config.models.chat.headers
9407
+ headers: config.models.chat.headers,
9408
+ byokProvider: config.models.chat.byokProvider,
9409
+ byokHeaders: config.models.chat.byokHeaders,
9410
+ byokApiShape: config.models.chat.byokApiShape
9066
9411
  });
9067
9412
  if (!jsonMode) {
9068
- console.log(chalk30.green(" Guardian: enabled (notch-solace-lite)"));
9413
+ console.log(chalk30.green(` Guardian: enabled (${process.env.NOTCH_GUARDIAN_MODEL ?? config.models.chat.model})`));
9069
9414
  }
9070
9415
  } catch (err) {
9071
9416
  if (!jsonMode) {
@@ -9110,6 +9455,28 @@ ${repoMapStr}` : "",
9110
9455
  }
9111
9456
  }
9112
9457
  };
9458
+ const baseSystemPrompt = await buildSystemPrompt(config.projectRoot, activeModelId, toolCtx);
9459
+ const systemPrompt = coordinatorMode ? [
9460
+ COORDINATOR_SYSTEM_PROMPT,
9461
+ "\n\n",
9462
+ baseSystemPrompt,
9463
+ repoMapStr ? `
9464
+
9465
+ ## Repository Map
9466
+ ${repoMapStr}` : "",
9467
+ outputSchema ? `
9468
+
9469
+ ${schemaInstructions(outputSchema)}` : ""
9470
+ ].join("") : [
9471
+ baseSystemPrompt,
9472
+ repoMapStr ? `
9473
+
9474
+ ## Repository Map
9475
+ ${repoMapStr}` : "",
9476
+ outputSchema ? `
9477
+
9478
+ ${schemaInstructions(outputSchema)}` : ""
9479
+ ].join("");
9113
9480
  await toolCtx.runHook?.("session-start", {});
9114
9481
  const stopFileWatcher = startFileWatcher(config.projectRoot, hookConfig, (event, results) => {
9115
9482
  for (const r of results) {
@@ -9181,9 +9548,10 @@ Analyze the above input.`;
9181
9548
  attachments.push(res);
9182
9549
  }
9183
9550
  if (attachments.length > 0 && !modelSupportsImages(activeModelId)) {
9184
- const msg = `Selected model "${activeModelId}" may not support images. Switch with --model or --provider.`;
9185
- if (jsonMode) events.emit({ type: "warning", message: msg });
9186
- else console.warn(chalk30.yellow(` ${msg}`));
9551
+ const msg = `Selected model "${activeModelId}" does not support image attachments. Switch to a vision-capable model.`;
9552
+ if (jsonMode) events.emit({ type: "error", message: msg });
9553
+ else console.error(chalk30.red(` ${msg}`));
9554
+ process.exit(1);
9187
9555
  }
9188
9556
  if (attachments.length > 0) {
9189
9557
  const imageBlocks = attachments.map(imageToContentBlock);
@@ -9391,11 +9759,9 @@ Analyze the above input.`;
9391
9759
  } catch {
9392
9760
  }
9393
9761
  }
9394
- for (const client of mcpClients) {
9395
- try {
9396
- await client.disconnect();
9397
- } catch {
9398
- }
9762
+ try {
9763
+ disconnectMCPServers();
9764
+ } catch {
9399
9765
  }
9400
9766
  slashMenu.cleanup();
9401
9767
  stopActiveLoop();
@@ -9422,35 +9788,6 @@ Analyze the above input.`;
9422
9788
  const picked = await interactiveModelPicker(activeModelId);
9423
9789
  if (!picked) {
9424
9790
  console.log(chalk30.gray(" Cancelled\n"));
9425
- } else if (picked.kind === "notch") {
9426
- if (picked.id === activeModelId) {
9427
- console.log(chalk30.gray(` Already using ${MODEL_CATALOG[picked.id].label}
9428
- `));
9429
- } else {
9430
- activeModelId = picked.id;
9431
- config.models.chat.model = activeModelId;
9432
- config.models.chat.byokProvider = void 0;
9433
- try {
9434
- model = resolveModel(config.models.chat);
9435
- const switchedInfo = MODEL_CATALOG[picked.id];
9436
- console.log(chalk30.green(` \u2713 Switched to ${switchedInfo.label} (${switchedInfo.id})`));
9437
- const shortName = picked.id.replace("notch-", "");
9438
- const hw = switchedInfo.hardware;
9439
- console.log(chalk30.gray(
9440
- ` Hosted inference is live. To run locally you need ~${hw.vramGb}GB VRAM (${switchedInfo.hardware.recommendedGpu}) + ${hw.diskGb}GB disk for Q4 weights.`
9441
- ));
9442
- if (switchedInfo.hfRepo) {
9443
- console.log(chalk30.gray(` Run ${chalk30.white(`/model download ${shortName}`)} to pull them in the background.
9444
- `));
9445
- } else {
9446
- console.log(chalk30.gray(` Local weights aren't published yet \u2014 sticking with the hosted API.
9447
- `));
9448
- }
9449
- } catch (e) {
9450
- console.log(chalk30.red(` Failed to switch: ${e.message}
9451
- `));
9452
- }
9453
- }
9454
9791
  } else {
9455
9792
  activeModelId = picked.modelRef;
9456
9793
  config.models.chat.model = picked.modelRef;
@@ -9474,12 +9811,12 @@ Analyze the above input.`;
9474
9811
  rl.prompt();
9475
9812
  return;
9476
9813
  }
9477
- if (input.startsWith("/model ")) {
9814
+ if (input.startsWith("/model ") && !input.startsWith("/model download")) {
9478
9815
  const arg = input.replace("/model ", "").trim();
9479
9816
  if (isByokRef(arg)) {
9480
9817
  const { provider } = parseByokRef(arg);
9481
9818
  if (!findByokProvider(provider)) {
9482
- console.log(chalk30.red(` Unknown BYOK provider: ${provider}`));
9819
+ console.log(chalk30.red(` Unknown provider: ${provider}`));
9483
9820
  console.log(chalk30.gray(` Run /providers to list built-ins.`));
9484
9821
  rl.prompt();
9485
9822
  return;
@@ -9498,6 +9835,29 @@ Analyze the above input.`;
9498
9835
  `));
9499
9836
  } else {
9500
9837
  console.log(chalk30.red(` Failed to switch: ${e.message}
9838
+ `));
9839
+ }
9840
+ }
9841
+ rl.prompt();
9842
+ return;
9843
+ }
9844
+ const currentProvider = activeByokProvider(config.models.chat);
9845
+ if (currentProvider) {
9846
+ const providerId = currentProvider.id === "__custom__" ? "custom" : currentProvider.id;
9847
+ const modelRef = `${providerId}/${arg}`;
9848
+ activeModelId = modelRef;
9849
+ config.models.chat.model = modelRef;
9850
+ config.models.chat.byokProvider = void 0;
9851
+ try {
9852
+ model = resolveModel(config.models.chat);
9853
+ console.log(chalk30.green(` Switched to ${currentProvider.label} (${modelRef})
9854
+ `));
9855
+ } catch (e) {
9856
+ if (e instanceof ByokMissingApiKeyError) {
9857
+ console.log(chalk30.yellow(` \u26A0 ${e.message}
9858
+ `));
9859
+ } else {
9860
+ console.log(chalk30.red(` Failed to switch: ${e.message}
9501
9861
  `));
9502
9862
  }
9503
9863
  }
@@ -9510,8 +9870,8 @@ Analyze the above input.`;
9510
9870
  }
9511
9871
  if (!isValidModel(newModel)) {
9512
9872
  console.log(chalk30.red(` Unknown model: ${arg}`));
9513
- console.log(chalk30.gray(` Notch models: ${modelChoices}`));
9514
- console.log(chalk30.gray(` For BYOK use "<provider>:<model>" (e.g. openrouter:anthropic/claude-sonnet-4-6). Run /providers to list built-ins.`));
9873
+ console.log(chalk30.gray(" Use provider/model, for example openrouter/anthropic/claude-sonnet-4-6."));
9874
+ console.log(chalk30.gray(" Run /providers to list built-ins."));
9515
9875
  rl.prompt();
9516
9876
  return;
9517
9877
  }
@@ -9526,52 +9886,16 @@ Analyze the above input.`;
9526
9886
  return;
9527
9887
  }
9528
9888
  if (input.startsWith("/model download")) {
9529
- const arg = input.replace("/model download", "").trim();
9530
- if (!arg) {
9531
- console.log(chalk30.red(" Usage: /model download <pyre|ignis|solace|solace-lite>"));
9532
- rl.prompt();
9533
- return;
9534
- }
9535
- const normalised = arg.startsWith("notch-") ? arg : `notch-${arg}`;
9536
- if (!isValidModel(normalised)) {
9537
- console.log(chalk30.red(` Unknown model: ${arg}`));
9538
- console.log(chalk30.gray(` Notch models: ${modelChoices}`));
9539
- rl.prompt();
9540
- return;
9541
- }
9542
- const info2 = MODEL_CATALOG[normalised];
9543
- const { probeSystemCapabilities, evaluateHardware, renderVerdict, startModelDownload } = await import("./model-download-3NDKS3VM.js");
9544
- const caps = await probeSystemCapabilities();
9545
- const verdict = evaluateHardware(info2, caps);
9546
- console.log(`
9547
- ${renderVerdict(info2, verdict)}
9548
- `);
9549
- if (!info2.hfRepo) {
9550
- console.log(chalk30.yellow(
9551
- ` ${info2.label} isn't published to Hugging Face yet \u2014 the Notch team is still preparing merged weights for public download. Use the hosted API in the meantime (run ${chalk30.white("notch login")} if you haven't already).
9552
- `
9553
- ));
9554
- rl.prompt();
9555
- return;
9556
- }
9557
- try {
9558
- const handle = await startModelDownload(normalised);
9559
- console.log(chalk30.green(` \u2193 Downloading ${info2.label} in background (pid ${handle.pid}).`));
9560
- console.log(chalk30.gray(` Cache: ${handle.cacheDir}`));
9561
- console.log(chalk30.gray(` Poll status with ${chalk30.white("/downloads")} \u2014 the pull keeps running even if you exit the REPL (${info2.hardware.diskGb} GB transfer).
9562
- `));
9563
- } catch (err) {
9564
- console.log(chalk30.red(` Download failed to start: ${err.message}
9565
- `));
9566
- }
9889
+ console.log(chalk30.gray(" Direct weight downloads are no longer part of the model picker."));
9890
+ console.log(chalk30.gray(" Run a local provider instead, then select it with /model ollama/<model>, /model lmstudio/<model>, or /model custom/<model>.\n"));
9567
9891
  rl.prompt();
9568
9892
  return;
9569
9893
  }
9570
9894
  if (input === "/downloads") {
9571
- const { listDownloads } = await import("./model-download-3NDKS3VM.js");
9895
+ const { listDownloads } = await import("./model-download-KCQJCEPW.js");
9572
9896
  const active = listDownloads();
9573
9897
  if (active.length === 0) {
9574
- console.log(chalk30.gray(" No downloads started this session. Try: /model download pyre\n"));
9898
+ console.log(chalk30.gray(" No local downloads started this session.\n"));
9575
9899
  } else {
9576
9900
  console.log(chalk30.gray("\n Model State Last line"));
9577
9901
  for (const h of active) {
@@ -9594,13 +9918,13 @@ ${renderVerdict(info2, verdict)}
9594
9918
  rl.prompt();
9595
9919
  return;
9596
9920
  }
9597
- const spinner2 = ora7("Pulling BYOK keys from freesyntax.dev...").start();
9921
+ const spinner2 = ora7("Pulling provider keys from freesyntax.dev...").start();
9598
9922
  try {
9599
9923
  const synced = await syncByokKeys(creds.token);
9600
9924
  spinner2.stop();
9601
9925
  const providers = Object.keys(synced.keys);
9602
9926
  if (providers.length === 0) {
9603
- console.log(chalk30.gray(` No BYOK keys on your profile yet. Add them at https://freesyntax.dev/settings/keys.
9927
+ console.log(chalk30.gray(` No provider keys on your profile yet. Add them at https://freesyntax.dev/settings/keys.
9604
9928
  `));
9605
9929
  } else {
9606
9930
  console.log(chalk30.green(` \u2713 Synced ${providers.length} key(s): ${providers.join(", ")}`));
@@ -10204,9 +10528,10 @@ ${renderVerdict(info2, verdict)}
10204
10528
  if (replAttachments.length > 0 && !modelSupportsImages(activeModelId)) {
10205
10529
  console.warn(
10206
10530
  chalk30.yellow(
10207
- ` \u26A0 ${activeModelId} may not support images. Switch with /model.`
10531
+ ` \u26A0 ${activeModelId} does not support image attachments. Switch with /model; skipping image(s).`
10208
10532
  )
10209
10533
  );
10534
+ replAttachments.length = 0;
10210
10535
  }
10211
10536
  const textForRefs = imgRefs.cleanedText || (replAttachments.length > 0 ? "Describe the attached image(s)." : "");
10212
10537
  const { cleanInput, references } = await resolveReferences(textForRefs, config.projectRoot);
@@ -10215,6 +10540,10 @@ ${renderVerdict(info2, verdict)}
10215
10540
  if (references.length > 0) {
10216
10541
  console.log(chalk30.gray(` Injected ${references.length} reference(s)`));
10217
10542
  }
10543
+ if (!finalPrompt.trim() && replAttachments.length === 0) {
10544
+ console.warn(chalk30.yellow(" Add a message, or switch to a vision-capable model before sending only images."));
10545
+ return;
10546
+ }
10218
10547
  if (replAttachments.length > 0) {
10219
10548
  const imageBlocks = replAttachments.map(imageToContentBlock);
10220
10549
  messages.push({
@@ -10390,7 +10719,6 @@ async function handleRalphSubcommand(args, cliOpts) {
10390
10719
  const config = await loadConfig(cliOpts.cwd ? { projectRoot: cliOpts.cwd } : {});
10391
10720
  if (cliOpts.model) config.models.chat.model = cliOpts.model;
10392
10721
  const model = resolveModel(config.models.chat);
10393
- const systemPrompt = await buildSystemPrompt(config.projectRoot, config.models.chat.model);
10394
10722
  const toolCtx = {
10395
10723
  cwd: config.projectRoot,
10396
10724
  requireConfirm: false,