@freesyntax/notch-cli 0.5.21 → 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.
Files changed (38) hide show
  1. package/dist/{apply-patch-D5PDUXUC.js → apply-patch-U6K67CMT.js} +1 -0
  2. package/dist/auth-UAMMP5IJ.js +29 -0
  3. package/dist/{chunk-TU465P2P.js → chunk-474TAHDN.js} +50 -8
  4. package/dist/{chunk-OSWUX6TC.js → chunk-4HPRBCSY.js} +1 -1
  5. package/dist/{chunk-QKM27RHS.js → chunk-6NKRMZTX.js} +1 -1
  6. package/dist/{chunk-443G6HCC.js → chunk-JVFOAPYV.js} +331 -79
  7. package/dist/chunk-KCAR5DOB.js +52 -0
  8. package/dist/chunk-KFQGP6VL.js +33 -0
  9. package/dist/chunk-O6AKZ4OH.js +0 -0
  10. package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
  11. package/dist/chunk-UHK6SI4H.js +206 -0
  12. package/dist/{chunk-MMBFNIKE.js → chunk-YNYVQ7ZI.js} +10 -8
  13. package/dist/{compression-SQAIQ2UU.js → compression-YJLWEHCC.js} +1 -0
  14. package/dist/config-set-5F4VK7IT.js +111 -0
  15. package/dist/{edit-JEFEK43H.js → edit-6QYAXVNU.js} +1 -0
  16. package/dist/{git-5T5TSQTX.js → git-DNQ5EELH.js} +1 -0
  17. package/dist/{github-DWRGWX6U.js → github-34T4QQIH.js} +1 -0
  18. package/dist/{glob-BI3P4C7Q.js → glob-XT43LEJ4.js} +1 -0
  19. package/dist/{grep-VZ3I5GNW.js → grep-T2CXYNRI.js} +1 -0
  20. package/dist/index.js +878 -428
  21. package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
  22. package/dist/model-download-KCQJCEPW.js +176 -0
  23. package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
  24. package/dist/{ollama-bench-QQHBIG2D.js → ollama-bench-JLC5POG3.js} +8 -4
  25. package/dist/{ollama-launch-2ASVER3S.js → ollama-launch-3IKB2A3Z.js} +6 -2
  26. package/dist/{ollama-usage-2WPCZJJI.js → ollama-usage-3PROM2WC.js} +1 -0
  27. package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
  28. package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
  29. package/dist/server-GMF4WV67.js +187 -0
  30. package/dist/{session-index-SSGOOZXK.js → session-index-7FWEVP6E.js} +3 -2
  31. package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
  32. package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
  33. package/dist/{tools-7WAWS6V4.js → tools-ABRZPCEJ.js} +6 -3
  34. package/dist/{web-fetch-KNIV3Z3W.js → web-fetch-OTNDICGJ.js} +1 -0
  35. package/dist/{write-NNHLOTYK.js → write-ZOSB7I4J.js} +1 -0
  36. package/package.json +60 -57
  37. package/dist/auth-JQX6MHJG.js +0 -16
  38. package/dist/server-7UQKCB2Z.js +0 -1477
package/dist/index.js CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-OSWUX6TC.js";
2
+ 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-TU465P2P.js";
16
+ } from "./chunk-474TAHDN.js";
15
17
  import {
16
18
  Rollout,
17
19
  generateSessionId,
@@ -20,16 +22,21 @@ import {
20
22
  readIndex,
21
23
  readRollout,
22
24
  rebuildMessagesFromRollout
23
- } from "./chunk-QKM27RHS.js";
25
+ } from "./chunk-6NKRMZTX.js";
24
26
  import {
25
27
  autoCompress,
26
28
  estimateTokens
27
29
  } from "./chunk-PKZKVOAN.js";
30
+ import "./chunk-O6AKZ4OH.js";
31
+ import {
32
+ loadConfig,
33
+ persistConfigPatch
34
+ } from "./chunk-UHK6SI4H.js";
35
+ import "./chunk-KCAR5DOB.js";
28
36
  import {
29
37
  ByokMissingApiKeyError,
30
38
  ByokMissingBaseUrlError,
31
39
  MODEL_CATALOG,
32
- MODEL_IDS,
33
40
  MissingApiKeyError,
34
41
  findByokProvider,
35
42
  isByokRef,
@@ -37,10 +44,11 @@ import {
37
44
  listByokProviders,
38
45
  modelSupportsImages,
39
46
  parseByokRef,
40
- readOllamaCreds,
47
+ recordShadowUsage,
41
48
  resolveModel,
42
49
  validateConfig
43
- } from "./chunk-443G6HCC.js";
50
+ } from "./chunk-JVFOAPYV.js";
51
+ import "./chunk-GFVLHUSS.js";
44
52
  import "./chunk-6CZCFY6H.js";
45
53
  import "./chunk-6U3ZAGYA.js";
46
54
  import "./chunk-FFB7GK3Y.js";
@@ -57,10 +65,12 @@ import {
57
65
  registerCommand
58
66
  } from "./chunk-3QUV4JEX.js";
59
67
  import {
68
+ auth_exports,
60
69
  clearCredentials,
70
+ init_auth,
61
71
  loadCredentials,
62
72
  login
63
- } from "./chunk-FIFC4V2R.js";
73
+ } from "./chunk-PPEBWOMJ.js";
64
74
  import "./chunk-CQMAVWLJ.js";
65
75
  import "./chunk-O3WZW7GS.js";
66
76
  import "./chunk-YAYPQTOU.js";
@@ -69,6 +79,9 @@ import {
69
79
  } from "./chunk-C4CPDDMN.js";
70
80
  import "./chunk-W4FAGQFL.js";
71
81
  import "./chunk-FAULT7VE.js";
82
+ import {
83
+ __toCommonJS
84
+ } from "./chunk-KFQGP6VL.js";
72
85
 
73
86
  // src/index.ts
74
87
  import { Command } from "commander";
@@ -77,105 +90,8 @@ import ora7 from "ora";
77
90
  import * as readline from "readline";
78
91
  import * as nodePath2 from "path";
79
92
 
80
- // src/config.ts
81
- import fs from "fs/promises";
82
- import path from "path";
83
- var DEFAULT_MODEL = {
84
- model: "notch-pyre",
85
- temperature: 0.3
86
- };
87
- var DEFAULTS = {
88
- models: { chat: DEFAULT_MODEL },
89
- projectRoot: process.cwd(),
90
- autoConfirm: false,
91
- maxIterations: 25,
92
- useRepoMap: true,
93
- renderMarkdown: true,
94
- enableMemory: true,
95
- enableHooks: true,
96
- permissionMode: "auto",
97
- theme: "default"
98
- };
99
- async function loadConfig(overrides = {}) {
100
- const config = { ...DEFAULTS, models: { chat: { ...DEFAULT_MODEL } } };
101
- const configPath = path.resolve(config.projectRoot, ".notch.json");
102
- try {
103
- const raw = await fs.readFile(configPath, "utf-8");
104
- const fileConfig = JSON.parse(raw);
105
- if (fileConfig.model && (isValidModel(fileConfig.model) || isByokRef(fileConfig.model))) {
106
- config.models.chat.model = fileConfig.model;
107
- }
108
- if (fileConfig.baseUrl) config.models.chat.baseUrl = fileConfig.baseUrl;
109
- if (fileConfig.apiKey) config.models.chat.apiKey = fileConfig.apiKey;
110
- if (fileConfig.byok && typeof fileConfig.byok === "object") {
111
- const byok = fileConfig.byok;
112
- config.byok = { ...byok };
113
- if (byok.provider && findByokProvider(byok.provider === "custom" ? "__custom__" : byok.provider)) {
114
- config.models.chat.byokProvider = byok.provider === "custom" ? "__custom__" : byok.provider;
115
- if (byok.model) config.models.chat.model = byok.model;
116
- if (byok.baseUrl) config.models.chat.baseUrl = byok.baseUrl;
117
- if (byok.headers) {
118
- config.models.chat.byokHeaders = { ...config.models.chat.byokHeaders, ...byok.headers };
119
- }
120
- if (byok.apiShape === "openai" || byok.apiShape === "anthropic") {
121
- config.models.chat.byokApiShape = byok.apiShape;
122
- }
123
- }
124
- }
125
- if (fileConfig.hybrid && typeof fileConfig.hybrid === "object") {
126
- const hybrid = fileConfig.hybrid;
127
- config.hybrid = hybrid;
128
- }
129
- if (fileConfig.maxIterations) config.maxIterations = fileConfig.maxIterations;
130
- if (fileConfig.useRepoMap !== void 0) config.useRepoMap = fileConfig.useRepoMap;
131
- if (fileConfig.temperature !== void 0) config.models.chat.temperature = fileConfig.temperature;
132
- if (fileConfig.renderMarkdown !== void 0) config.renderMarkdown = fileConfig.renderMarkdown;
133
- if (fileConfig.enableMemory !== void 0) config.enableMemory = fileConfig.enableMemory;
134
- if (fileConfig.enableHooks !== void 0) config.enableHooks = fileConfig.enableHooks;
135
- if (fileConfig.permissionMode) config.permissionMode = fileConfig.permissionMode;
136
- if (fileConfig.shellTimeout) config.shellTimeout = fileConfig.shellTimeout;
137
- if (fileConfig.theme) config.theme = fileConfig.theme;
138
- } catch {
139
- }
140
- const activeProviderId = config.models.chat.byokProvider ?? (typeof config.models.chat.model === "string" && isByokRef(config.models.chat.model) ? config.models.chat.model.split(":", 1)[0] : void 0);
141
- const isOllamaProvider = activeProviderId === "ollama" || activeProviderId === "ollama-cloud" || activeProviderId === "ollama-anthropic";
142
- if (isOllamaProvider) {
143
- if (!config.models.chat.apiKey && !process.env.OLLAMA_API_KEY) {
144
- const ollamaCreds = await readOllamaCreds();
145
- if (ollamaCreds?.apiKey) {
146
- config.models.chat.apiKey = ollamaCreds.apiKey;
147
- }
148
- }
149
- } else {
150
- const creds = await loadCredentials();
151
- if (creds?.token) {
152
- config.models.chat.apiKey = creds.token;
153
- }
154
- }
155
- if (process.env.NOTCH_MODEL) {
156
- const envModel = process.env.NOTCH_MODEL;
157
- if (isValidModel(envModel)) {
158
- config.models.chat.model = envModel;
159
- } else if (isByokRef(envModel)) {
160
- config.models.chat.model = envModel;
161
- config.models.chat.byokProvider = void 0;
162
- }
163
- }
164
- if (process.env.NOTCH_BASE_URL) {
165
- config.models.chat.baseUrl = process.env.NOTCH_BASE_URL;
166
- }
167
- if (process.env.NOTCH_API_KEY) {
168
- config.models.chat.apiKey = process.env.NOTCH_API_KEY;
169
- }
170
- if (config.models.chat.temperature !== void 0) {
171
- config.models.chat.temperature = Math.max(0, Math.min(2, config.models.chat.temperature));
172
- }
173
- config.maxIterations = Math.max(1, Math.min(100, config.maxIterations));
174
- return { ...config, ...overrides };
175
- }
176
-
177
93
  // src/ui/image-input.ts
178
- import { promises as fs2 } from "fs";
94
+ import { promises as fs } from "fs";
179
95
  import * as nodePath from "path";
180
96
  import * as os from "os";
181
97
  function isImageLoadError(r) {
@@ -292,7 +208,7 @@ async function loadFromFile(spec, cwd) {
292
208
  const abs = expandUserPath(spec, cwd);
293
209
  let stat;
294
210
  try {
295
- stat = await fs2.stat(abs);
211
+ stat = await fs.stat(abs);
296
212
  } catch (err) {
297
213
  return { error: `Image file not found: ${abs} (${err?.code ?? err?.message ?? "unknown error"})` };
298
214
  }
@@ -309,7 +225,7 @@ async function loadFromFile(spec, cwd) {
309
225
  }
310
226
  let buf;
311
227
  try {
312
- buf = await fs2.readFile(abs);
228
+ buf = await fs.readFile(abs);
313
229
  } catch (err) {
314
230
  return { error: `Failed to read ${abs}: ${err?.message ?? String(err)}` };
315
231
  }
@@ -386,19 +302,19 @@ function formatAttachmentStatus(att) {
386
302
 
387
303
  // src/agent/loop.ts
388
304
  import { streamText } from "ai";
389
- import { promises as fs5 } from "fs";
305
+ import { promises as fs4 } from "fs";
390
306
  import { execSync } from "child_process";
391
307
 
392
308
  // src/context/project-instructions.ts
393
- import fs3 from "fs/promises";
394
- import path2 from "path";
309
+ import fs2 from "fs/promises";
310
+ import path from "path";
395
311
  import os2 from "os";
396
312
  var INSTRUCTION_FILES = [".notch.md", "NOTCH.md", ".notch/instructions.md"];
397
313
  async function loadProjectInstructions(projectRoot) {
398
314
  const sources = [];
399
315
  const homeDir = os2.homedir();
400
316
  for (const file of INSTRUCTION_FILES) {
401
- const globalPath = path2.join(homeDir, file);
317
+ const globalPath = path.join(homeDir, file);
402
318
  const content = await safeRead(globalPath);
403
319
  if (content) {
404
320
  sources.push({ path: globalPath, content, scope: "global" });
@@ -406,7 +322,7 @@ async function loadProjectInstructions(projectRoot) {
406
322
  }
407
323
  }
408
324
  for (const file of INSTRUCTION_FILES) {
409
- const projectPath = path2.join(projectRoot, file);
325
+ const projectPath = path.join(projectRoot, file);
410
326
  const content = await safeRead(projectPath);
411
327
  if (content) {
412
328
  sources.push({ path: projectPath, content, scope: "project" });
@@ -428,7 +344,7 @@ ${sections.join("\n\n")}`;
428
344
  }
429
345
  async function safeRead(filePath) {
430
346
  try {
431
- const content = await fs3.readFile(filePath, "utf-8");
347
+ const content = await fs2.readFile(filePath, "utf-8");
432
348
  return content.trim() || null;
433
349
  } catch {
434
350
  return null;
@@ -436,19 +352,19 @@ async function safeRead(filePath) {
436
352
  }
437
353
 
438
354
  // src/memory/store.ts
439
- import fs4 from "fs/promises";
440
- import path3 from "path";
355
+ import fs3 from "fs/promises";
356
+ import path2 from "path";
441
357
  import os3 from "os";
442
- var MEMORY_DIR = path3.join(os3.homedir(), ".notch", "memory");
443
- var INDEX_FILE = path3.join(MEMORY_DIR, "MEMORY.md");
358
+ var MEMORY_DIR = path2.join(os3.homedir(), ".notch", "memory");
359
+ var INDEX_FILE = path2.join(MEMORY_DIR, "MEMORY.md");
444
360
  async function ensureDir() {
445
- await fs4.mkdir(MEMORY_DIR, { recursive: true });
361
+ await fs3.mkdir(MEMORY_DIR, { recursive: true });
446
362
  }
447
363
  async function saveMemory(memory) {
448
364
  await ensureDir();
449
365
  const slug = memory.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
450
366
  const filename = `${memory.type}_${slug}.md`;
451
- const filePath = path3.join(MEMORY_DIR, filename);
367
+ const filePath = path2.join(MEMORY_DIR, filename);
452
368
  const fileContent = [
453
369
  "---",
454
370
  `name: ${memory.name}`,
@@ -459,18 +375,18 @@ async function saveMemory(memory) {
459
375
  "",
460
376
  memory.content
461
377
  ].join("\n");
462
- await fs4.writeFile(filePath, fileContent, "utf-8");
378
+ await fs3.writeFile(filePath, fileContent, "utf-8");
463
379
  await updateIndex();
464
380
  return filename;
465
381
  }
466
382
  async function loadMemories() {
467
383
  await ensureDir();
468
- const files = await fs4.readdir(MEMORY_DIR);
384
+ const files = await fs3.readdir(MEMORY_DIR);
469
385
  const memories = [];
470
386
  for (const file of files) {
471
387
  if (!file.endsWith(".md") || file === "MEMORY.md") continue;
472
388
  try {
473
- const content = await fs4.readFile(path3.join(MEMORY_DIR, file), "utf-8");
389
+ const content = await fs3.readFile(path2.join(MEMORY_DIR, file), "utf-8");
474
390
  const memory = parseMemoryFile(content, file);
475
391
  if (memory) memories.push(memory);
476
392
  } catch {
@@ -480,7 +396,7 @@ async function loadMemories() {
480
396
  }
481
397
  async function deleteMemory(filename) {
482
398
  try {
483
- await fs4.unlink(path3.join(MEMORY_DIR, filename));
399
+ await fs3.unlink(path2.join(MEMORY_DIR, filename));
484
400
  await updateIndex();
485
401
  return true;
486
402
  } catch {
@@ -556,7 +472,7 @@ async function updateIndex() {
556
472
  }
557
473
  lines.push("");
558
474
  }
559
- await fs4.writeFile(INDEX_FILE, lines.join("\n"), "utf-8");
475
+ await fs3.writeFile(INDEX_FILE, lines.join("\n"), "utf-8");
560
476
  }
561
477
 
562
478
  // src/agent/prompt-sections.ts
@@ -705,6 +621,304 @@ function microcompact(messages, opts2 = {}) {
705
621
  return { messages: out, cleared, savedChars };
706
622
  }
707
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
+
708
922
  // src/agent/loop.ts
709
923
  function getErrorSignature(toolName, result) {
710
924
  return {
@@ -731,7 +945,18 @@ async function runAgentLoop(messages, config) {
731
945
  let wasCompressed = false;
732
946
  const recentErrors = [];
733
947
  const MAX_REPEATED_ERRORS = 3;
948
+ const correctionCfg = config.correction ?? autoDetectPreset(config.modelId ?? "");
949
+ let totalCorrectionAttempts = 0;
950
+ let intentResult = "unclear";
734
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") : "";
735
960
  await config.toolContext.runHook?.("pre-compact", { messageCount: history.length });
736
961
  history = await autoCompress(history, config.model, contextWindow, () => {
737
962
  wasCompressed = true;
@@ -740,6 +965,13 @@ async function runAgentLoop(messages, config) {
740
965
  if (wasCompressed) {
741
966
  await config.toolContext.runHook?.("post-compact", { messageCount: history.length });
742
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
+ }
743
975
  while (iterations < maxIter) {
744
976
  iterations++;
745
977
  const mc = microcompact(history, { keepLastN: 6, threshold: 50 });
@@ -753,7 +985,7 @@ async function runAgentLoop(messages, config) {
753
985
  let streamUsage = null;
754
986
  const result = streamText({
755
987
  model: config.model,
756
- system: config.systemPrompt,
988
+ system: `${config.systemPrompt}${templateFormatHint}`,
757
989
  messages: history,
758
990
  tools,
759
991
  maxSteps: 1
@@ -792,8 +1024,67 @@ async function runAgentLoop(messages, config) {
792
1024
  if (streamUsage) {
793
1025
  totalPromptTokens += streamUsage.promptTokens ?? 0;
794
1026
  totalCompletionTokens += streamUsage.completionTokens ?? 0;
1027
+ void recordShadowUsage(
1028
+ config.model,
1029
+ streamUsage
1030
+ );
795
1031
  }
796
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
+ }
797
1088
  if (toolCalls.length > 0) {
798
1089
  let hasRepeatedError = false;
799
1090
  for (const tr of toolResults) {
@@ -859,6 +1150,7 @@ async function runAgentLoop(messages, config) {
859
1150
  iterations,
860
1151
  toolCallCount: totalToolCalls,
861
1152
  compressed: wasCompressed,
1153
+ correctionAttempts: totalCorrectionAttempts,
862
1154
  usage: {
863
1155
  promptTokens: totalPromptTokens,
864
1156
  completionTokens: totalCompletionTokens,
@@ -874,6 +1166,7 @@ async function runAgentLoop(messages, config) {
874
1166
  iterations,
875
1167
  toolCallCount: totalToolCalls,
876
1168
  compressed: wasCompressed,
1169
+ correctionAttempts: totalCorrectionAttempts,
877
1170
  usage: {
878
1171
  promptTokens: totalPromptTokens,
879
1172
  completionTokens: totalCompletionTokens,
@@ -881,7 +1174,7 @@ async function runAgentLoop(messages, config) {
881
1174
  }
882
1175
  };
883
1176
  }
884
- async function buildSystemPrompt(projectRoot, _modelId) {
1177
+ async function buildSystemPrompt(projectRoot, _modelId, toolContext) {
885
1178
  const sections = [
886
1179
  // =========================================================
887
1180
  // CACHEABLE PREFIX — stable across turns, sessions, users.
@@ -892,7 +1185,7 @@ async function buildSystemPrompt(projectRoot, _modelId) {
892
1185
  () => [
893
1186
  "You are Notch, an expert AI coding assistant built by Driftrail.",
894
1187
  "You help developers write, debug, refactor, and understand code.",
895
- "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."
896
1189
  ].join("\n")
897
1190
  ),
898
1191
  safeSection(
@@ -928,8 +1221,11 @@ async function buildSystemPrompt(projectRoot, _modelId) {
928
1221
  "- When the user asks for a fix, fix the root cause, not the symptom."
929
1222
  ].join("\n")
930
1223
  ),
931
- safeSection("tools-list", () => `## Available Tools
932
- ${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
+ ),
933
1229
  safeSection("builtin-agents", () => {
934
1230
  try {
935
1231
  const agents = listBuiltinAgents();
@@ -954,7 +1250,7 @@ ${describeTools()}`),
954
1250
  lines.push(`- Platform: ${process.platform}`);
955
1251
  lines.push(`- Current date (UTC): ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
956
1252
  try {
957
- const entries = await fs5.readdir(projectRoot);
1253
+ const entries = await fs4.readdir(projectRoot);
958
1254
  const preview = entries.filter((e) => !e.startsWith(".")).slice(0, 30).join(", ");
959
1255
  if (preview) lines.push(`- Top-level entries: ${preview}`);
960
1256
  } catch {
@@ -1009,8 +1305,8 @@ ${memoryStr}`, {
1009
1305
  }
1010
1306
 
1011
1307
  // src/agent/checkpoints.ts
1012
- import fs6 from "fs/promises";
1013
- import path4 from "path";
1308
+ import fs5 from "fs/promises";
1309
+ import path3 from "path";
1014
1310
  var CheckpointManager = class {
1015
1311
  checkpoints = [];
1016
1312
  nextId = 1;
@@ -1019,7 +1315,7 @@ var CheckpointManager = class {
1019
1315
  async recordBefore(filePath) {
1020
1316
  if (this.pendingFiles.has(filePath)) return;
1021
1317
  try {
1022
- const content = await fs6.readFile(filePath, "utf-8");
1318
+ const content = await fs5.readFile(filePath, "utf-8");
1023
1319
  this.pendingFiles.set(filePath, content);
1024
1320
  } catch {
1025
1321
  this.pendingFiles.set(filePath, null);
@@ -1031,7 +1327,7 @@ var CheckpointManager = class {
1031
1327
  for (const [filePath, before] of this.pendingFiles) {
1032
1328
  let after = null;
1033
1329
  try {
1034
- after = await fs6.readFile(filePath, "utf-8");
1330
+ after = await fs5.readFile(filePath, "utf-8");
1035
1331
  } catch {
1036
1332
  }
1037
1333
  files.push({ path: filePath, before, after });
@@ -1053,12 +1349,12 @@ var CheckpointManager = class {
1053
1349
  for (const snap of checkpoint.files) {
1054
1350
  if (snap.before === null) {
1055
1351
  try {
1056
- await fs6.unlink(snap.path);
1352
+ await fs5.unlink(snap.path);
1057
1353
  } catch {
1058
1354
  }
1059
1355
  } else {
1060
- await fs6.mkdir(path4.dirname(snap.path), { recursive: true });
1061
- await fs6.writeFile(snap.path, snap.before, "utf-8");
1356
+ await fs5.mkdir(path3.dirname(snap.path), { recursive: true });
1357
+ await fs5.writeFile(snap.path, snap.before, "utf-8");
1062
1358
  }
1063
1359
  }
1064
1360
  return checkpoint;
@@ -1090,8 +1386,8 @@ var CheckpointManager = class {
1090
1386
  }
1091
1387
  }
1092
1388
  }
1093
- return Array.from(fileMap.entries()).map(([path21, { before, after }]) => ({
1094
- path: path21,
1389
+ return Array.from(fileMap.entries()).map(([path20, { before, after }]) => ({
1390
+ path: path20,
1095
1391
  before,
1096
1392
  after
1097
1393
  }));
@@ -1489,14 +1785,14 @@ var CostTracker = class {
1489
1785
  };
1490
1786
 
1491
1787
  // src/agent/ralph.ts
1492
- import fs8 from "fs/promises";
1493
- import path6 from "path";
1788
+ import fs7 from "fs/promises";
1789
+ import path5 from "path";
1494
1790
  import chalk4 from "chalk";
1495
1791
  import { generateText as generateText2, streamText as streamText2 } from "ai";
1496
1792
 
1497
1793
  // src/context/repo-map.ts
1498
- import fs7 from "fs/promises";
1499
- import path5 from "path";
1794
+ import fs6 from "fs/promises";
1795
+ import path4 from "path";
1500
1796
  import { glob } from "glob";
1501
1797
  var PATTERNS = {
1502
1798
  ts: [
@@ -1527,7 +1823,7 @@ var PATTERNS = {
1527
1823
  };
1528
1824
  var IMPORT_PATTERN = /(?:import|from)\s+['"]([^'"]+)['"]/g;
1529
1825
  function getPatterns(filePath) {
1530
- const ext2 = path5.extname(filePath).slice(1);
1826
+ const ext2 = path4.extname(filePath).slice(1);
1531
1827
  if (["ts", "tsx", "mts", "cts"].includes(ext2)) return PATTERNS.ts;
1532
1828
  if (["js", "jsx", "mjs", "cjs"].includes(ext2)) return PATTERNS.js;
1533
1829
  if (ext2 === "py") return PATTERNS.py;
@@ -1607,9 +1903,9 @@ async function buildRepoMap(root) {
1607
1903
  const entries = [];
1608
1904
  files.sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
1609
1905
  for (const file of files.slice(0, 300)) {
1610
- const fullPath = path5.resolve(root, file);
1906
+ const fullPath = path4.resolve(root, file);
1611
1907
  try {
1612
- const content = await fs7.readFile(fullPath, "utf-8");
1908
+ const content = await fs6.readFile(fullPath, "utf-8");
1613
1909
  const lines = content.split("\n").length;
1614
1910
  const patterns = getPatterns(file);
1615
1911
  const symbols = extractSymbols(content, patterns);
@@ -1711,11 +2007,11 @@ ${repoContext || "(empty project)"}`;
1711
2007
  async function savePlan(plan, cwd) {
1712
2008
  plan.updated = (/* @__PURE__ */ new Date()).toISOString();
1713
2009
  plan.completedCount = plan.tasks.filter((t) => t.status === "done").length;
1714
- await fs8.writeFile(path6.join(cwd, PLAN_FILE), JSON.stringify(plan, null, 2), "utf-8");
2010
+ await fs7.writeFile(path5.join(cwd, PLAN_FILE), JSON.stringify(plan, null, 2), "utf-8");
1715
2011
  }
1716
2012
  async function loadPlan(cwd) {
1717
2013
  try {
1718
- const raw = await fs8.readFile(path6.join(cwd, PLAN_FILE), "utf-8");
2014
+ const raw = await fs7.readFile(path5.join(cwd, PLAN_FILE), "utf-8");
1719
2015
  return JSON.parse(raw);
1720
2016
  } catch {
1721
2017
  return null;
@@ -1723,7 +2019,7 @@ async function loadPlan(cwd) {
1723
2019
  }
1724
2020
  async function deletePlan(cwd) {
1725
2021
  try {
1726
- await fs8.unlink(path6.join(cwd, PLAN_FILE));
2022
+ await fs7.unlink(path5.join(cwd, PLAN_FILE));
1727
2023
  } catch {
1728
2024
  }
1729
2025
  }
@@ -1758,8 +2054,7 @@ ${toolContext.cwd}
1758
2054
  ## Repository
1759
2055
  ${repoContext}
1760
2056
 
1761
- ## Available Tools
1762
- ${describeTools()}
2057
+ ${describeToolSchemas(toolContext)}
1763
2058
 
1764
2059
  ## Instructions
1765
2060
  1. Read relevant files to understand the current state
@@ -1906,8 +2201,8 @@ function formatRalphStatus(plan) {
1906
2201
  }
1907
2202
 
1908
2203
  // src/context/references.ts
1909
- import fs9 from "fs/promises";
1910
- import path7 from "path";
2204
+ import fs8 from "fs/promises";
2205
+ import path6 from "path";
1911
2206
  import { glob as glob2 } from "glob";
1912
2207
  async function resolveReferences(input, cwd) {
1913
2208
  const references = [];
@@ -1950,9 +2245,9 @@ ${truncated}
1950
2245
  return sections.join("\n\n") + "\n\n";
1951
2246
  }
1952
2247
  async function resolveFile(ref, cwd) {
1953
- const filePath = path7.isAbsolute(ref) ? ref : path7.resolve(cwd, ref);
2248
+ const filePath = path6.isAbsolute(ref) ? ref : path6.resolve(cwd, ref);
1954
2249
  try {
1955
- const content = await fs9.readFile(filePath, "utf-8");
2250
+ const content = await fs8.readFile(filePath, "utf-8");
1956
2251
  const lines = content.split("\n");
1957
2252
  const numbered = lines.map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
1958
2253
  return {
@@ -2677,17 +2972,17 @@ function formatTokens(n) {
2677
2972
  }
2678
2973
 
2679
2974
  // src/ui/update-checker.ts
2680
- import fs10 from "fs/promises";
2681
- import path8 from "path";
2975
+ import fs9 from "fs/promises";
2976
+ import path7 from "path";
2682
2977
  import os4 from "os";
2683
2978
  import { execSync as execSync2, spawnSync } from "child_process";
2684
2979
  var PACKAGE_NAME = "@freesyntax/notch-cli";
2685
2980
  var CHECK_INTERVAL_HOURS = 4;
2686
2981
  var CHECK_INTERVAL_MS = CHECK_INTERVAL_HOURS * 60 * 60 * 1e3;
2687
- var NOTCH_DIR = path8.join(os4.homedir(), ".notch");
2688
- var CACHE_FILE = path8.join(NOTCH_DIR, "update-check.json");
2689
- var LOG_FILE = path8.join(NOTCH_DIR, "update-log.txt");
2690
- var CONFIG_FILE = path8.join(NOTCH_DIR, "config.json");
2982
+ var NOTCH_DIR = path7.join(os4.homedir(), ".notch");
2983
+ var CACHE_FILE = path7.join(NOTCH_DIR, "update-check.json");
2984
+ var LOG_FILE = path7.join(NOTCH_DIR, "update-log.txt");
2985
+ var CONFIG_FILE = path7.join(NOTCH_DIR, "config.json");
2691
2986
  var MAX_LOG_LINES = 500;
2692
2987
  var REGISTRY_BASE = "https://registry.npmjs.org";
2693
2988
  var FETCH_TIMEOUT_MS2 = 5e3;
@@ -2822,7 +3117,7 @@ async function saveChannel(channel) {
2822
3117
  }
2823
3118
  async function readUserConfig() {
2824
3119
  try {
2825
- const raw = await fs10.readFile(CONFIG_FILE, "utf-8");
3120
+ const raw = await fs9.readFile(CONFIG_FILE, "utf-8");
2826
3121
  const parsed = JSON.parse(raw);
2827
3122
  return typeof parsed === "object" && parsed ? parsed : null;
2828
3123
  } catch {
@@ -2831,7 +3126,7 @@ async function readUserConfig() {
2831
3126
  }
2832
3127
  async function loadCache() {
2833
3128
  try {
2834
- const raw = await fs10.readFile(CACHE_FILE, "utf-8");
3129
+ const raw = await fs9.readFile(CACHE_FILE, "utf-8");
2835
3130
  const parsed = JSON.parse(raw);
2836
3131
  if (parsed && typeof parsed === "object" && typeof parsed.lastCheck === "number" && (parsed.latestVersion === null || typeof parsed.latestVersion === "string")) {
2837
3132
  const channel = isValidChannel(parsed.channel) ? parsed.channel : "latest";
@@ -2858,7 +3153,7 @@ async function detectInstallLocation() {
2858
3153
  if (!entry) return "unknown";
2859
3154
  let entryReal;
2860
3155
  try {
2861
- entryReal = await fs10.realpath(entry);
3156
+ entryReal = await fs9.realpath(entry);
2862
3157
  } catch {
2863
3158
  entryReal = entry;
2864
3159
  }
@@ -2867,11 +3162,11 @@ async function detectInstallLocation() {
2867
3162
  return "global";
2868
3163
  }
2869
3164
  let dir = process.cwd();
2870
- const root = path8.parse(dir).root;
3165
+ const root = path7.parse(dir).root;
2871
3166
  while (dir !== root) {
2872
- const localNM = path8.join(dir, "node_modules");
3167
+ const localNM = path7.join(dir, "node_modules");
2873
3168
  if (pathIsInside(entryReal, localNM)) return "local";
2874
- const parent = path8.dirname(dir);
3169
+ const parent = path7.dirname(dir);
2875
3170
  if (parent === dir) break;
2876
3171
  dir = parent;
2877
3172
  }
@@ -2881,8 +3176,8 @@ async function detectInstallLocation() {
2881
3176
  }
2882
3177
  function pathIsInside(child, parent) {
2883
3178
  if (!child || !parent) return false;
2884
- const rel = path8.relative(path8.resolve(parent), path8.resolve(child));
2885
- return !!rel && !rel.startsWith("..") && !path8.isAbsolute(rel);
3179
+ const rel = path7.relative(path7.resolve(parent), path7.resolve(child));
3180
+ return !!rel && !rel.startsWith("..") && !path7.isAbsolute(rel);
2886
3181
  }
2887
3182
  async function tryExec(cmd) {
2888
3183
  try {
@@ -2940,7 +3235,7 @@ function splitPre(v) {
2940
3235
  return [v.slice(0, i), v.slice(i + 1)];
2941
3236
  }
2942
3237
  async function ensureNotchDir() {
2943
- await fs10.mkdir(NOTCH_DIR, { recursive: true });
3238
+ await fs9.mkdir(NOTCH_DIR, { recursive: true });
2944
3239
  }
2945
3240
  async function appendLog(message) {
2946
3241
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}`;
@@ -2951,7 +3246,7 @@ async function appendLogRaw(line) {
2951
3246
  await ensureNotchDir();
2952
3247
  let existing = "";
2953
3248
  try {
2954
- existing = await fs10.readFile(LOG_FILE, "utf-8");
3249
+ existing = await fs9.readFile(LOG_FILE, "utf-8");
2955
3250
  } catch {
2956
3251
  existing = "";
2957
3252
  }
@@ -2964,16 +3259,16 @@ async function appendLogRaw(line) {
2964
3259
  }
2965
3260
  async function atomicWrite(file, contents) {
2966
3261
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
2967
- await fs10.writeFile(tmp, contents, "utf-8");
3262
+ await fs9.writeFile(tmp, contents, "utf-8");
2968
3263
  try {
2969
- await fs10.rename(tmp, file);
3264
+ await fs9.rename(tmp, file);
2970
3265
  } catch (err) {
2971
3266
  try {
2972
- await fs10.copyFile(tmp, file);
2973
- await fs10.unlink(tmp).catch(() => {
3267
+ await fs9.copyFile(tmp, file);
3268
+ await fs9.unlink(tmp).catch(() => {
2974
3269
  });
2975
3270
  } catch {
2976
- await fs10.unlink(tmp).catch(() => {
3271
+ await fs9.unlink(tmp).catch(() => {
2977
3272
  });
2978
3273
  throw err;
2979
3274
  }
@@ -2995,16 +3290,16 @@ function indent(block, spaces = 2) {
2995
3290
  // src/commands/update.ts
2996
3291
  import chalk7 from "chalk";
2997
3292
  import { createRequire } from "module";
2998
- import path9 from "path";
3293
+ import path8 from "path";
2999
3294
  import { fileURLToPath } from "url";
3000
3295
  var _require = createRequire(import.meta.url);
3001
3296
  function resolveVersion() {
3002
3297
  try {
3003
3298
  const entry = process.argv[1];
3004
- let dir = entry ? path9.dirname(entry) : fileURLToPath(new URL(".", import.meta.url));
3005
- const root = path9.parse(dir).root;
3299
+ let dir = entry ? path8.dirname(entry) : fileURLToPath(new URL(".", import.meta.url));
3300
+ const root = path8.parse(dir).root;
3006
3301
  while (dir && dir !== root) {
3007
- const candidate = path9.join(dir, "package.json");
3302
+ const candidate = path8.join(dir, "package.json");
3008
3303
  try {
3009
3304
  const pkg = _require(candidate);
3010
3305
  if (pkg && pkg.name === PACKAGE_NAME && typeof pkg.version === "string") {
@@ -3012,7 +3307,7 @@ function resolveVersion() {
3012
3307
  }
3013
3308
  } catch {
3014
3309
  }
3015
- const parent = path9.dirname(dir);
3310
+ const parent = path8.dirname(dir);
3016
3311
  if (parent === dir) break;
3017
3312
  dir = parent;
3018
3313
  }
@@ -3134,8 +3429,8 @@ async function runUpdateCli(argv) {
3134
3429
  }
3135
3430
 
3136
3431
  // src/permissions/index.ts
3137
- import fs11 from "fs/promises";
3138
- import path11 from "path";
3432
+ import fs10 from "fs/promises";
3433
+ import path10 from "path";
3139
3434
  import os5 from "os";
3140
3435
 
3141
3436
  // node_modules/balanced-match/dist/esm/index.js
@@ -4196,11 +4491,11 @@ var qmarksTestNoExtDot = ([$0]) => {
4196
4491
  return (f) => f.length === len && f !== "." && f !== "..";
4197
4492
  };
4198
4493
  var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
4199
- var path10 = {
4494
+ var path9 = {
4200
4495
  win32: { sep: "\\" },
4201
4496
  posix: { sep: "/" }
4202
4497
  };
4203
- var sep = defaultPlatform === "win32" ? path10.win32.sep : path10.posix.sep;
4498
+ var sep = defaultPlatform === "win32" ? path9.win32.sep : path9.posix.sep;
4204
4499
  minimatch.sep = sep;
4205
4500
  var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **");
4206
4501
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -4997,11 +5292,11 @@ var DEFAULT_PERMISSIONS = {
4997
5292
  denyWriteGlobs: [...DEFAULT_DENY_WRITE_GLOBS]
4998
5293
  };
4999
5294
  async function loadPermissions(projectRoot) {
5000
- const projectPath = path11.join(projectRoot, ".notch.json");
5001
- const globalPath = path11.join(os5.homedir(), ".notch", "permissions.json");
5295
+ const projectPath = path10.join(projectRoot, ".notch.json");
5296
+ const globalPath = path10.join(os5.homedir(), ".notch", "permissions.json");
5002
5297
  let config = { ...DEFAULT_PERMISSIONS };
5003
5298
  try {
5004
- const raw = await fs11.readFile(globalPath, "utf-8");
5299
+ const raw = await fs10.readFile(globalPath, "utf-8");
5005
5300
  const parsed = JSON.parse(raw);
5006
5301
  if (parsed.permissions) {
5007
5302
  config = mergePermissions(config, parsed.permissions);
@@ -5009,7 +5304,7 @@ async function loadPermissions(projectRoot) {
5009
5304
  } catch {
5010
5305
  }
5011
5306
  try {
5012
- const raw = await fs11.readFile(projectPath, "utf-8");
5307
+ const raw = await fs10.readFile(projectPath, "utf-8");
5013
5308
  const parsed = JSON.parse(raw);
5014
5309
  if (parsed.permissions) {
5015
5310
  config = mergePermissions(config, parsed.permissions);
@@ -5149,17 +5444,17 @@ function mergePermissions(base, override) {
5149
5444
 
5150
5445
  // src/hooks/index.ts
5151
5446
  import { execSync as execSync3 } from "child_process";
5152
- import fs12 from "fs/promises";
5447
+ import fs11 from "fs/promises";
5153
5448
  import { watch } from "fs";
5154
- import path12 from "path";
5449
+ import path11 from "path";
5155
5450
  import os6 from "os";
5156
5451
  import crypto from "crypto";
5157
- var TRUST_STORE_PATH = path12.join(os6.homedir(), ".notch", "trusted-projects.json");
5452
+ var TRUST_STORE_PATH = path11.join(os6.homedir(), ".notch", "trusted-projects.json");
5158
5453
  async function isTrustedProject(projectRoot, raw) {
5159
5454
  const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
5160
- const key = path12.resolve(projectRoot);
5455
+ const key = path11.resolve(projectRoot);
5161
5456
  try {
5162
- const store = JSON.parse(await fs12.readFile(TRUST_STORE_PATH, "utf-8"));
5457
+ const store = JSON.parse(await fs11.readFile(TRUST_STORE_PATH, "utf-8"));
5163
5458
  return store[key] === fingerprint;
5164
5459
  } catch {
5165
5460
  return false;
@@ -5167,30 +5462,30 @@ async function isTrustedProject(projectRoot, raw) {
5167
5462
  }
5168
5463
  async function trustProject(projectRoot, raw) {
5169
5464
  const fingerprint = crypto.createHash("sha256").update(raw).digest("hex");
5170
- const key = path12.resolve(projectRoot);
5465
+ const key = path11.resolve(projectRoot);
5171
5466
  let store = {};
5172
5467
  try {
5173
- store = JSON.parse(await fs12.readFile(TRUST_STORE_PATH, "utf-8"));
5468
+ store = JSON.parse(await fs11.readFile(TRUST_STORE_PATH, "utf-8"));
5174
5469
  } catch {
5175
5470
  }
5176
5471
  store[key] = fingerprint;
5177
- await fs12.mkdir(path12.dirname(TRUST_STORE_PATH), { recursive: true });
5178
- await fs12.writeFile(TRUST_STORE_PATH, JSON.stringify(store, null, 2));
5472
+ await fs11.mkdir(path11.dirname(TRUST_STORE_PATH), { recursive: true });
5473
+ await fs11.writeFile(TRUST_STORE_PATH, JSON.stringify(store, null, 2));
5179
5474
  }
5180
5475
  async function loadHooks(projectRoot, promptTrust) {
5181
5476
  const hooks = [];
5182
- const globalPath = path12.join(os6.homedir(), ".notch", "hooks.json");
5477
+ const globalPath = path11.join(os6.homedir(), ".notch", "hooks.json");
5183
5478
  try {
5184
- const raw = await fs12.readFile(globalPath, "utf-8");
5479
+ const raw = await fs11.readFile(globalPath, "utf-8");
5185
5480
  const parsed = JSON.parse(raw);
5186
5481
  if (Array.isArray(parsed.hooks)) {
5187
5482
  hooks.push(...parsed.hooks);
5188
5483
  }
5189
5484
  } catch {
5190
5485
  }
5191
- const projectPath = path12.join(projectRoot, ".notch.json");
5486
+ const projectPath = path11.join(projectRoot, ".notch.json");
5192
5487
  try {
5193
- const raw = await fs12.readFile(projectPath, "utf-8");
5488
+ const raw = await fs11.readFile(projectPath, "utf-8");
5194
5489
  const parsed = JSON.parse(raw);
5195
5490
  if (Array.isArray(parsed.hooks) && parsed.hooks.length > 0) {
5196
5491
  const alreadyTrusted = await isTrustedProject(projectRoot, raw);
@@ -5285,7 +5580,7 @@ function startFileWatcher(projectRoot, hookConfig, onHookResult) {
5285
5580
  if (existing) clearTimeout(existing);
5286
5581
  pending.set(filename, setTimeout(async () => {
5287
5582
  pending.delete(filename);
5288
- const filePath = path12.join(projectRoot, filename);
5583
+ const filePath = path11.join(projectRoot, filename);
5289
5584
  const context = { cwd: projectRoot, file: filePath };
5290
5585
  const { results } = await runHooks(hookConfig, "file-changed", context);
5291
5586
  onHookResult?.("file-changed", results);
@@ -5305,8 +5600,8 @@ function startFileWatcher(projectRoot, hookConfig, onHookResult) {
5305
5600
  }
5306
5601
 
5307
5602
  // src/session/index.ts
5308
- import fs13 from "fs/promises";
5309
- import path13 from "path";
5603
+ import fs12 from "fs/promises";
5604
+ import path12 from "path";
5310
5605
  import os7 from "os";
5311
5606
 
5312
5607
  // src/session/fork.ts
@@ -5316,13 +5611,13 @@ import fsp from "fs/promises";
5316
5611
  import fsp2 from "fs/promises";
5317
5612
 
5318
5613
  // src/session/index.ts
5319
- var SESSION_DIR = path13.join(os7.homedir(), ".notch", "sessions");
5614
+ var SESSION_DIR = path12.join(os7.homedir(), ".notch", "sessions");
5320
5615
  var MAX_SESSIONS = 20;
5321
5616
  async function ensureDir2() {
5322
- await fs13.mkdir(SESSION_DIR, { recursive: true });
5617
+ await fs12.mkdir(SESSION_DIR, { recursive: true });
5323
5618
  }
5324
5619
  function sessionPath(id) {
5325
- return path13.join(SESSION_DIR, `${id}.json`);
5620
+ return path12.join(SESSION_DIR, `${id}.json`);
5326
5621
  }
5327
5622
  function generateId() {
5328
5623
  const now = /* @__PURE__ */ new Date();
@@ -5349,13 +5644,13 @@ async function saveSession(messages, project, model, existingId) {
5349
5644
  },
5350
5645
  messages
5351
5646
  };
5352
- await fs13.writeFile(sessionPath(id), JSON.stringify(session, null, 2), "utf-8");
5647
+ await fs12.writeFile(sessionPath(id), JSON.stringify(session, null, 2), "utf-8");
5353
5648
  await pruneOldSessions();
5354
5649
  return id;
5355
5650
  }
5356
5651
  async function loadSession(id) {
5357
5652
  try {
5358
- const raw = await fs13.readFile(sessionPath(id), "utf-8");
5653
+ const raw = await fs12.readFile(sessionPath(id), "utf-8");
5359
5654
  return JSON.parse(raw);
5360
5655
  } catch {
5361
5656
  return null;
@@ -5363,12 +5658,12 @@ async function loadSession(id) {
5363
5658
  }
5364
5659
  async function listSessions() {
5365
5660
  await ensureDir2();
5366
- const files = await fs13.readdir(SESSION_DIR);
5661
+ const files = await fs12.readdir(SESSION_DIR);
5367
5662
  const sessions = [];
5368
5663
  for (const file of files) {
5369
5664
  if (!file.endsWith(".json")) continue;
5370
5665
  try {
5371
- const raw = await fs13.readFile(path13.join(SESSION_DIR, file), "utf-8");
5666
+ const raw = await fs12.readFile(path12.join(SESSION_DIR, file), "utf-8");
5372
5667
  const session = JSON.parse(raw);
5373
5668
  sessions.push(session.meta);
5374
5669
  } catch {
@@ -5384,7 +5679,7 @@ async function loadLastSession(project) {
5384
5679
  }
5385
5680
  async function deleteSession(id) {
5386
5681
  try {
5387
- await fs13.unlink(sessionPath(id));
5682
+ await fs12.unlink(sessionPath(id));
5388
5683
  return true;
5389
5684
  } catch {
5390
5685
  return false;
@@ -5438,17 +5733,17 @@ async function exportSession(messages, outputPath, meta) {
5438
5733
  lines.push("");
5439
5734
  }
5440
5735
  }
5441
- await fs13.writeFile(outputPath, lines.join("\n"), "utf-8");
5736
+ await fs12.writeFile(outputPath, lines.join("\n"), "utf-8");
5442
5737
  return outputPath;
5443
5738
  }
5444
5739
 
5445
5740
  // src/init.ts
5446
- import fs14 from "fs/promises";
5447
- import path14 from "path";
5741
+ import fs13 from "fs/promises";
5742
+ import path13 from "path";
5448
5743
  import chalk8 from "chalk";
5449
5744
  async function fileExists(p) {
5450
5745
  try {
5451
- await fs14.access(p);
5746
+ await fs13.access(p);
5452
5747
  return true;
5453
5748
  } catch {
5454
5749
  return false;
@@ -5456,18 +5751,18 @@ async function fileExists(p) {
5456
5751
  }
5457
5752
  async function writeIfMissing(p, content, ctx) {
5458
5753
  if (await fileExists(p)) {
5459
- ctx.log(chalk8.gray(` Skipped ${path14.relative(ctx.projectRoot, p)} (already exists)`));
5754
+ ctx.log(chalk8.gray(` Skipped ${path13.relative(ctx.projectRoot, p)} (already exists)`));
5460
5755
  return false;
5461
5756
  }
5462
- await fs14.mkdir(path14.dirname(p), { recursive: true });
5463
- await fs14.writeFile(p, content, "utf-8");
5464
- ctx.log(chalk8.green(` Created ${path14.relative(ctx.projectRoot, p)}`));
5757
+ await fs13.mkdir(path13.dirname(p), { recursive: true });
5758
+ await fs13.writeFile(p, content, "utf-8");
5759
+ ctx.log(chalk8.green(` Created ${path13.relative(ctx.projectRoot, p)}`));
5465
5760
  return true;
5466
5761
  }
5467
5762
  async function patchJson(p, patch, ctx) {
5468
5763
  let current = {};
5469
5764
  if (await fileExists(p)) {
5470
- const raw = await fs14.readFile(p, "utf-8");
5765
+ const raw = await fs13.readFile(p, "utf-8");
5471
5766
  try {
5472
5767
  current = JSON.parse(raw);
5473
5768
  } catch {
@@ -5475,35 +5770,35 @@ async function patchJson(p, patch, ctx) {
5475
5770
  }
5476
5771
  }
5477
5772
  const next = patch(current);
5478
- await fs14.writeFile(p, JSON.stringify(next, null, 2) + "\n", "utf-8");
5479
- ctx.log(chalk8.green(` Patched ${path14.relative(ctx.projectRoot, p)}`));
5773
+ await fs13.writeFile(p, JSON.stringify(next, null, 2) + "\n", "utf-8");
5774
+ ctx.log(chalk8.green(` Patched ${path13.relative(ctx.projectRoot, p)}`));
5480
5775
  }
5481
5776
  async function ensureGitignoreEntries(p, entries, sectionTitle, ctx) {
5482
5777
  if (!await fileExists(p)) return;
5483
- const current = await fs14.readFile(p, "utf-8");
5778
+ const current = await fs13.readFile(p, "utf-8");
5484
5779
  const missing = entries.filter((e) => !current.includes(e));
5485
5780
  if (missing.length === 0) return;
5486
5781
  const append = `
5487
5782
  # ${sectionTitle}
5488
5783
  ` + missing.join("\n") + "\n";
5489
- await fs14.appendFile(p, append, "utf-8");
5784
+ await fs13.appendFile(p, append, "utf-8");
5490
5785
  ctx.log(chalk8.green(` Updated .gitignore (+${missing.length})`));
5491
5786
  }
5492
5787
  var DEFAULT_CONFIG = {
5493
- model: "notch-pyre",
5788
+ model: "openrouter/anthropic/claude-sonnet-4-6",
5494
5789
  temperature: 0.3,
5495
5790
  maxIterations: 25,
5496
5791
  useRepoMap: true,
5497
5792
  renderMarkdown: true,
5498
5793
  permissionMode: "auto",
5499
- // BYOK (Bring-Your-Own-Key) passthrough — disabled by default.
5500
- // 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
5501
5796
  // request to an OpenAI-compatible endpoint (OpenAI / Anthropic /
5502
- // OpenRouter / Together / Fireworks / Groq / Ollama / vLLM / LM Studio
5797
+ // OpenRouter / Google / DeepSeek / Together / Fireworks / Groq / Ollama / vLLM / LM Studio
5503
5798
  // / custom). Providers read their key from the corresponding env var —
5504
5799
  // e.g. OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY.
5505
5800
  // Run `notch --list-providers` to see all built-in ids.
5506
- _byokExample: {
5801
+ _providerExample: {
5507
5802
  provider: "openrouter",
5508
5803
  model: "anthropic/claude-sonnet-4-6"
5509
5804
  }
@@ -5536,17 +5831,17 @@ var baseInstaller = {
5536
5831
  required: true,
5537
5832
  async run(ctx) {
5538
5833
  await writeIfMissing(
5539
- path14.join(ctx.projectRoot, ".notch.json"),
5834
+ path13.join(ctx.projectRoot, ".notch.json"),
5540
5835
  JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n",
5541
5836
  ctx
5542
5837
  );
5543
5838
  await writeIfMissing(
5544
- path14.join(ctx.projectRoot, ".notch.md"),
5839
+ path13.join(ctx.projectRoot, ".notch.md"),
5545
5840
  DEFAULT_INSTRUCTIONS,
5546
5841
  ctx
5547
5842
  );
5548
5843
  await ensureGitignoreEntries(
5549
- path14.join(ctx.projectRoot, ".gitignore"),
5844
+ path13.join(ctx.projectRoot, ".gitignore"),
5550
5845
  [".notch.json"],
5551
5846
  "Notch CLI",
5552
5847
  ctx
@@ -5559,7 +5854,7 @@ var mcpInstaller = {
5559
5854
  description: "Add the mcpServers block to .notch.json (example: github)",
5560
5855
  async run(ctx) {
5561
5856
  await patchJson(
5562
- path14.join(ctx.projectRoot, ".notch.json"),
5857
+ path13.join(ctx.projectRoot, ".notch.json"),
5563
5858
  (cur) => ({
5564
5859
  ...cur,
5565
5860
  mcpServers: cur.mcpServers ?? {
@@ -5580,7 +5875,7 @@ var hooksInstaller = {
5580
5875
  description: "Add hooks array scaffold to .notch.json",
5581
5876
  async run(ctx) {
5582
5877
  await patchJson(
5583
- path14.join(ctx.projectRoot, ".notch.json"),
5878
+ path13.join(ctx.projectRoot, ".notch.json"),
5584
5879
  (cur) => ({
5585
5880
  ...cur,
5586
5881
  hooks: cur.hooks ?? [
@@ -5596,10 +5891,10 @@ var vestaInstaller = {
5596
5891
  label: "Vesta agents",
5597
5892
  description: "Scaffold .notch/agents/ directory for Vesta agent configs",
5598
5893
  async run(ctx) {
5599
- const dir = path14.join(ctx.projectRoot, ".notch", "agents");
5600
- await fs14.mkdir(dir, { recursive: true });
5894
+ const dir = path13.join(ctx.projectRoot, ".notch", "agents");
5895
+ await fs13.mkdir(dir, { recursive: true });
5601
5896
  await writeIfMissing(
5602
- path14.join(dir, "README.md"),
5897
+ path13.join(dir, "README.md"),
5603
5898
  `# Vesta Agents
5604
5899
 
5605
5900
  Drop agent JSON/TOML configs in this directory. Notch will load them at startup.
@@ -5614,7 +5909,7 @@ var oneSecInstaller = {
5614
5909
  description: "Enable 1-SEC opt-in and add the block to .notch.json",
5615
5910
  async run(ctx) {
5616
5911
  await patchJson(
5617
- path14.join(ctx.projectRoot, ".notch.json"),
5912
+ path13.join(ctx.projectRoot, ".notch.json"),
5618
5913
  (cur) => ({
5619
5914
  ...cur,
5620
5915
  security: cur.security ?? {
@@ -5633,7 +5928,7 @@ var telemetryInstaller = {
5633
5928
  description: "Enable opt-in anonymous usage stats",
5634
5929
  async run(ctx) {
5635
5930
  await patchJson(
5636
- path14.join(ctx.projectRoot, ".notch.json"),
5931
+ path13.join(ctx.projectRoot, ".notch.json"),
5637
5932
  (cur) => ({ ...cur, telemetry: { enabled: true, anonymous: true } }),
5638
5933
  ctx
5639
5934
  );
@@ -5645,7 +5940,7 @@ var agentsMdInstaller = {
5645
5940
  description: "Industry-standard agent instructions file (shared with Codex, Cursor, etc.)",
5646
5941
  async run(ctx) {
5647
5942
  await writeIfMissing(
5648
- path14.join(ctx.projectRoot, "AGENTS.md"),
5943
+ path13.join(ctx.projectRoot, "AGENTS.md"),
5649
5944
  `# Agent Instructions
5650
5945
 
5651
5946
  Shared instructions for AI coding agents working in this repo.
@@ -5753,6 +6048,9 @@ async function initProject(projectRoot, opts2 = {}) {
5753
6048
  `));
5754
6049
  }
5755
6050
 
6051
+ // src/index.ts
6052
+ init_auth();
6053
+
5756
6054
  // src/tools/diff-preview.ts
5757
6055
  function unifiedDiff(oldContent, newContent, filePath) {
5758
6056
  const t = theme();
@@ -5889,9 +6187,9 @@ var JsonEmitter = class {
5889
6187
  };
5890
6188
 
5891
6189
  // src/ui/output-schema.ts
5892
- import fs15 from "fs/promises";
6190
+ import fs14 from "fs/promises";
5893
6191
  async function loadOutputSchema(filePath) {
5894
- const raw = await fs15.readFile(filePath, "utf-8");
6192
+ const raw = await fs14.readFile(filePath, "utf-8");
5895
6193
  const schema = JSON.parse(raw);
5896
6194
  return { schema, raw };
5897
6195
  }
@@ -6108,8 +6406,8 @@ function isCoordinatorModeEnv() {
6108
6406
 
6109
6407
  // src/commands/doctor.ts
6110
6408
  import { execSync as execSync4 } from "child_process";
6111
- import fs16 from "fs/promises";
6112
- import path15 from "path";
6409
+ import fs15 from "fs/promises";
6410
+ import path14 from "path";
6113
6411
  import os8 from "os";
6114
6412
  import chalk9 from "chalk";
6115
6413
  async function runDiagnostics(cwd) {
@@ -6151,26 +6449,26 @@ async function runDiagnostics(cwd) {
6151
6449
  results.push({ name: "Config", status: "fail", message: `Could not load: ${err.message}` });
6152
6450
  }
6153
6451
  try {
6154
- await fs16.access(path15.join(cwd, ".notch.json"));
6452
+ await fs15.access(path14.join(cwd, ".notch.json"));
6155
6453
  results.push({ name: ".notch.json", status: "ok", message: "Found" });
6156
6454
  } catch {
6157
6455
  results.push({ name: ".notch.json", status: "warn", message: "Not found. Run: notch init" });
6158
6456
  }
6159
- const notchDir = path15.join(os8.homedir(), ".notch");
6457
+ const notchDir = path14.join(os8.homedir(), ".notch");
6160
6458
  try {
6161
- await fs16.access(notchDir);
6459
+ await fs15.access(notchDir);
6162
6460
  results.push({ name: "~/.notch/", status: "ok", message: "Exists" });
6163
6461
  } catch {
6164
6462
  results.push({ name: "~/.notch/", status: "warn", message: "Not found (will be created on first use)" });
6165
6463
  }
6166
6464
  try {
6167
- await fs16.access(path15.join(cwd, ".notch.md"));
6465
+ await fs15.access(path14.join(cwd, ".notch.md"));
6168
6466
  results.push({ name: ".notch.md", status: "ok", message: "Found" });
6169
6467
  } catch {
6170
6468
  results.push({ name: ".notch.md", status: "warn", message: "Not found. Run: notch init" });
6171
6469
  }
6172
6470
  try {
6173
- const configRaw = await fs16.readFile(path15.join(cwd, ".notch.json"), "utf-8").catch(() => "{}");
6471
+ const configRaw = await fs15.readFile(path14.join(cwd, ".notch.json"), "utf-8").catch(() => "{}");
6174
6472
  const mcpConfigs = parseMCPConfig(JSON.parse(configRaw));
6175
6473
  const serverNames = Object.keys(mcpConfigs);
6176
6474
  if (serverNames.length === 0) {
@@ -6191,8 +6489,8 @@ async function runDiagnostics(cwd) {
6191
6489
  results.push({ name: "MCP Servers", status: "ok", message: "No config to check" });
6192
6490
  }
6193
6491
  try {
6194
- const sessionsDir = path15.join(notchDir, "sessions");
6195
- const entries = await fs16.readdir(sessionsDir).catch(() => []);
6492
+ const sessionsDir = path14.join(notchDir, "sessions");
6493
+ const entries = await fs15.readdir(sessionsDir).catch(() => []);
6196
6494
  results.push({ name: "Sessions", status: "ok", message: `${entries.length} saved` });
6197
6495
  } catch {
6198
6496
  results.push({ name: "Sessions", status: "ok", message: "0 saved" });
@@ -6594,11 +6892,11 @@ Read the file first, then make the change. Only modify this one file.`
6594
6892
 
6595
6893
  // src/commands/plugin.ts
6596
6894
  import { execSync as execSync7, execFileSync as execFileSync2 } from "child_process";
6597
- import fs17 from "fs/promises";
6598
- import path16 from "path";
6895
+ import fs16 from "fs/promises";
6896
+ import path15 from "path";
6599
6897
  import os9 from "os";
6600
6898
  import chalk15 from "chalk";
6601
- var GLOBAL_PLUGINS_DIR = path16.join(os9.homedir(), ".notch", "plugins");
6899
+ var GLOBAL_PLUGINS_DIR = path15.join(os9.homedir(), ".notch", "plugins");
6602
6900
  registerCommand("/plugin", async (args, ctx) => {
6603
6901
  const parts = args.split(/\s+/);
6604
6902
  const subcommand = parts[0] || "list";
@@ -6634,8 +6932,8 @@ registerCommand("/plugin", async (args, ctx) => {
6634
6932
  console.log(chalk15.gray(" Usage: /plugin install <npm-package-or-git-url>\n"));
6635
6933
  return;
6636
6934
  }
6637
- await fs17.mkdir(GLOBAL_PLUGINS_DIR, { recursive: true });
6638
- const pluginDir = path16.join(GLOBAL_PLUGINS_DIR, path16.basename(target).replace(/\.git$/, ""));
6935
+ await fs16.mkdir(GLOBAL_PLUGINS_DIR, { recursive: true });
6936
+ const pluginDir = path15.join(GLOBAL_PLUGINS_DIR, path15.basename(target).replace(/\.git$/, ""));
6639
6937
  console.log(chalk15.gray(` Installing ${target}...`));
6640
6938
  try {
6641
6939
  if (target.includes("/") && !target.startsWith("@")) {
@@ -6645,7 +6943,7 @@ registerCommand("/plugin", async (args, ctx) => {
6645
6943
  stdio: "pipe"
6646
6944
  });
6647
6945
  } else {
6648
- await fs17.mkdir(pluginDir, { recursive: true });
6946
+ await fs16.mkdir(pluginDir, { recursive: true });
6649
6947
  execFileSync2("npm", ["init", "-y"], {
6650
6948
  cwd: pluginDir,
6651
6949
  encoding: "utf-8",
@@ -6660,7 +6958,7 @@ registerCommand("/plugin", async (args, ctx) => {
6660
6958
  });
6661
6959
  }
6662
6960
  try {
6663
- const pkgExists = await fs17.access(path16.join(pluginDir, "package.json")).then(() => true).catch(() => false);
6961
+ const pkgExists = await fs16.access(path15.join(pluginDir, "package.json")).then(() => true).catch(() => false);
6664
6962
  if (pkgExists) {
6665
6963
  execSync7("npm install --production", {
6666
6964
  cwd: pluginDir,
@@ -6686,10 +6984,10 @@ registerCommand("/plugin", async (args, ctx) => {
6686
6984
  console.log(chalk15.gray(" Usage: /plugin remove <plugin-name>\n"));
6687
6985
  return;
6688
6986
  }
6689
- const pluginDir = path16.join(GLOBAL_PLUGINS_DIR, target);
6987
+ const pluginDir = path15.join(GLOBAL_PLUGINS_DIR, target);
6690
6988
  try {
6691
- await fs17.access(pluginDir);
6692
- await fs17.rm(pluginDir, { recursive: true, force: true });
6989
+ await fs16.access(pluginDir);
6990
+ await fs16.rm(pluginDir, { recursive: true, force: true });
6693
6991
  console.log(chalk15.green(` \u2713 Removed ${target}`));
6694
6992
  console.log(chalk15.gray(" Restart notch to apply.\n"));
6695
6993
  } catch {
@@ -7301,10 +7599,10 @@ import ora4 from "ora";
7301
7599
 
7302
7600
  // src/skills/registry.ts
7303
7601
  import { createHash } from "crypto";
7304
- import fs18 from "fs";
7602
+ import fs17 from "fs";
7305
7603
  import fsp3 from "fs/promises";
7306
7604
  import os10 from "os";
7307
- import path17 from "path";
7605
+ import path16 from "path";
7308
7606
  var registry = /* @__PURE__ */ new Map();
7309
7607
  var loadPromises = /* @__PURE__ */ new Map();
7310
7608
  var extractedDirs = /* @__PURE__ */ new Set();
@@ -7342,14 +7640,14 @@ async function performLoad(skill) {
7342
7640
  function getExtractDir(skill) {
7343
7641
  const filesJson = skill.files ? JSON.stringify(skill.files) : "";
7344
7642
  const sha8 = createHash("sha256").update(filesJson).digest("hex").slice(0, 8);
7345
- return path17.join(os10.tmpdir(), "notch-skills", `${skill.id}-${sha8}`);
7643
+ return path16.join(os10.tmpdir(), "notch-skills", `${skill.id}-${sha8}`);
7346
7644
  }
7347
7645
  async function extractFiles(dir, files) {
7348
7646
  await fsp3.mkdir(dir, { recursive: true, mode: 448 });
7349
7647
  const byParent = /* @__PURE__ */ new Map();
7350
7648
  for (const [relPath, content] of Object.entries(files)) {
7351
7649
  const target = resolveSafePath(dir, relPath);
7352
- const parent = path17.dirname(target);
7650
+ const parent = path16.dirname(target);
7353
7651
  const group = byParent.get(parent);
7354
7652
  if (group) group.push([target, content]);
7355
7653
  else byParent.set(parent, [[target, content]]);
@@ -7366,17 +7664,17 @@ async function extractFiles(dir, files) {
7366
7664
  );
7367
7665
  }
7368
7666
  function resolveSafePath(baseDir, relPath) {
7369
- const normalized = path17.normalize(relPath);
7667
+ const normalized = path16.normalize(relPath);
7370
7668
  const parts = normalized.split(/[\\/]/);
7371
- if (path17.isAbsolute(normalized) || parts.includes("..")) {
7669
+ if (path16.isAbsolute(normalized) || parts.includes("..")) {
7372
7670
  throw new Error(`skill file path escapes skill dir: ${relPath}`);
7373
7671
  }
7374
- return path17.join(baseDir, normalized);
7672
+ return path16.join(baseDir, normalized);
7375
7673
  }
7376
7674
  function cleanupSkills() {
7377
7675
  for (const dir of extractedDirs) {
7378
7676
  try {
7379
- fs18.rmSync(dir, { recursive: true, force: true });
7677
+ fs17.rmSync(dir, { recursive: true, force: true });
7380
7678
  } catch {
7381
7679
  }
7382
7680
  }
@@ -7562,6 +7860,7 @@ registerCommand("/microcompact", async (args, ctx) => {
7562
7860
  import { exec, execSync as execSync11 } from "child_process";
7563
7861
  import chalk27 from "chalk";
7564
7862
  import ora5 from "ora";
7863
+ init_auth();
7565
7864
  var PLATFORM_URL = "https://freesyntax.dev";
7566
7865
  function openBrowser(url) {
7567
7866
  let cmd;
@@ -7693,6 +7992,7 @@ registerCommand("/cloud", async (args, ctx) => {
7693
7992
  import { exec as exec2 } from "child_process";
7694
7993
  import chalk28 from "chalk";
7695
7994
  import ora6 from "ora";
7995
+ init_auth();
7696
7996
  var PLATFORM_URL2 = "https://freesyntax.dev";
7697
7997
  function openBrowser2(url) {
7698
7998
  let cmd;
@@ -7775,8 +8075,8 @@ registerCommand("/agent-builder", async (args, _ctx) => {
7775
8075
  });
7776
8076
 
7777
8077
  // src/ui/completions.ts
7778
- import fs19 from "fs";
7779
- import path18 from "path";
8078
+ import fs18 from "fs";
8079
+ import path17 from "path";
7780
8080
  var COMMANDS = [
7781
8081
  "/help",
7782
8082
  "/quit",
@@ -7848,8 +8148,7 @@ function buildCompleter(cwd) {
7848
8148
  }
7849
8149
  if (line.startsWith("/model ")) {
7850
8150
  const partial = line.slice(7);
7851
- const modelNames = MODEL_IDS.map((id) => id.replace("notch-", ""));
7852
- const allNames = [...MODEL_IDS, ...modelNames];
8151
+ const allNames = listByokProviders().filter((p) => p.defaultModel).map((p) => `${p.id}/${p.defaultModel}`);
7853
8152
  const matches = allNames.filter((m) => m.startsWith(partial));
7854
8153
  return [matches.map((m) => `/model ${m}`), line];
7855
8154
  }
@@ -7869,15 +8168,15 @@ function buildCompleter(cwd) {
7869
8168
  }
7870
8169
  function completeFilePath(partial, cwd) {
7871
8170
  try {
7872
- const dir = partial.includes("/") ? path18.resolve(cwd, path18.dirname(partial)) : cwd;
7873
- const prefix = partial.includes("/") ? path18.basename(partial) : partial;
7874
- const entries = fs19.readdirSync(dir, { withFileTypes: true });
8171
+ const dir = partial.includes("/") ? path17.resolve(cwd, path17.dirname(partial)) : cwd;
8172
+ const prefix = partial.includes("/") ? path17.basename(partial) : partial;
8173
+ const entries = fs18.readdirSync(dir, { withFileTypes: true });
7875
8174
  const matches = [];
7876
8175
  for (const entry of entries) {
7877
8176
  if (entry.name.startsWith(".")) continue;
7878
8177
  if (entry.name === "node_modules" || entry.name === ".git") continue;
7879
8178
  if (entry.name.startsWith(prefix)) {
7880
- const relative = partial.includes("/") ? path18.dirname(partial) + "/" + entry.name : entry.name;
8179
+ const relative = partial.includes("/") ? path17.dirname(partial) + "/" + entry.name : entry.name;
7881
8180
  if (entry.isDirectory()) {
7882
8181
  matches.push(relative + "/");
7883
8182
  } else {
@@ -7901,6 +8200,8 @@ var SLASH_COMMANDS = [
7901
8200
  // Model & Status
7902
8201
  { name: "/model", description: "Switch or list models", category: "Model" },
7903
8202
  { name: "/status", description: "Check API endpoint health", category: "Model" },
8203
+ { name: "/providers", description: "List model providers", category: "Model" },
8204
+ { name: "/sync-keys", description: "Pull provider keys from freesyntax.dev", category: "Model" },
7904
8205
  // Session
7905
8206
  { name: "/save", description: "Save current session", category: "Session" },
7906
8207
  { name: "/sessions", description: "List saved sessions", category: "Session" },
@@ -8158,22 +8459,22 @@ function rewritePromptLine(rl) {
8158
8459
  }
8159
8460
 
8160
8461
  // src/services/autoDream/gate.ts
8161
- import fs20 from "fs/promises";
8162
- import path19 from "path";
8462
+ import fs19 from "fs/promises";
8463
+ import path18 from "path";
8163
8464
  import os11 from "os";
8164
- var NOTCH_DIR2 = path19.join(os11.homedir(), ".notch");
8165
- var SESSION_DIR2 = path19.join(NOTCH_DIR2, "sessions");
8166
- var STATE_FILE = path19.join(NOTCH_DIR2, "dream-state.json");
8167
- var LOCK_FILE = path19.join(NOTCH_DIR2, ".dream.lock");
8465
+ var NOTCH_DIR2 = path18.join(os11.homedir(), ".notch");
8466
+ var SESSION_DIR2 = path18.join(NOTCH_DIR2, "sessions");
8467
+ var STATE_FILE = path18.join(NOTCH_DIR2, "dream-state.json");
8468
+ var LOCK_FILE = path18.join(NOTCH_DIR2, ".dream.lock");
8168
8469
  var DEFAULT_MIN_HOURS = 24;
8169
8470
  var DEFAULT_MIN_SESSIONS = 5;
8170
8471
  var MS_PER_HOUR = 36e5;
8171
8472
  async function ensureNotchDir2() {
8172
- await fs20.mkdir(NOTCH_DIR2, { recursive: true });
8473
+ await fs19.mkdir(NOTCH_DIR2, { recursive: true });
8173
8474
  }
8174
8475
  async function readState() {
8175
8476
  try {
8176
- const raw = await fs20.readFile(STATE_FILE, "utf-8");
8477
+ const raw = await fs19.readFile(STATE_FILE, "utf-8");
8177
8478
  const parsed = JSON.parse(raw);
8178
8479
  if (typeof parsed?.lastRunAt !== "number" || !Number.isFinite(parsed.lastRunAt)) {
8179
8480
  return null;
@@ -8185,7 +8486,7 @@ async function readState() {
8185
8486
  }
8186
8487
  async function writeState(state) {
8187
8488
  await ensureNotchDir2();
8188
- await fs20.writeFile(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
8489
+ await fs19.writeFile(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
8189
8490
  }
8190
8491
  async function shouldDream(_cwd, opts2 = {}) {
8191
8492
  const minHours = opts2.minHours ?? DEFAULT_MIN_HOURS;
@@ -8201,7 +8502,7 @@ async function shouldDream(_cwd, opts2 = {}) {
8201
8502
  }
8202
8503
  let sessionFiles = [];
8203
8504
  try {
8204
- sessionFiles = await fs20.readdir(SESSION_DIR2);
8505
+ sessionFiles = await fs19.readdir(SESSION_DIR2);
8205
8506
  } catch {
8206
8507
  return { should: false, reason: "session-gate: no session directory yet" };
8207
8508
  }
@@ -8209,7 +8510,7 @@ async function shouldDream(_cwd, opts2 = {}) {
8209
8510
  for (const name of sessionFiles) {
8210
8511
  if (!name.endsWith(".json") && !name.endsWith(".jsonl")) continue;
8211
8512
  try {
8212
- const stat = await fs20.stat(path19.join(SESSION_DIR2, name));
8513
+ const stat = await fs19.stat(path18.join(SESSION_DIR2, name));
8213
8514
  if (stat.mtimeMs > lastAt) touchedCount++;
8214
8515
  } catch {
8215
8516
  }
@@ -8222,7 +8523,7 @@ async function shouldDream(_cwd, opts2 = {}) {
8222
8523
  }
8223
8524
  try {
8224
8525
  await ensureNotchDir2();
8225
- const handle = await fs20.open(LOCK_FILE, "wx");
8526
+ const handle = await fs19.open(LOCK_FILE, "wx");
8226
8527
  try {
8227
8528
  await handle.writeFile(String(process.pid), "utf-8");
8228
8529
  } finally {
@@ -8245,7 +8546,7 @@ async function shouldDream(_cwd, opts2 = {}) {
8245
8546
  }
8246
8547
  async function releaseDreamLock() {
8247
8548
  try {
8248
- await fs20.unlink(LOCK_FILE);
8549
+ await fs19.unlink(LOCK_FILE);
8249
8550
  } catch {
8250
8551
  }
8251
8552
  }
@@ -8262,10 +8563,10 @@ async function recordDreamRun() {
8262
8563
  }
8263
8564
 
8264
8565
  // src/services/autoDream/consolidationPrompt.ts
8265
- import path20 from "path";
8566
+ import path19 from "path";
8266
8567
  import os12 from "os";
8267
- var MEMORY_DIR2 = path20.join(os12.homedir(), ".notch", "memory");
8268
- var SESSION_DIR3 = path20.join(os12.homedir(), ".notch", "sessions");
8568
+ var MEMORY_DIR2 = path19.join(os12.homedir(), ".notch", "memory");
8569
+ var SESSION_DIR3 = path19.join(os12.homedir(), ".notch", "sessions");
8269
8570
  var INDEX_FILE2 = "MEMORY.md";
8270
8571
  var MAX_INDEX_LINES = 200;
8271
8572
  var MAX_INDEX_BYTES = 25 * 1024;
@@ -8427,21 +8728,25 @@ function isDisabled() {
8427
8728
  }
8428
8729
 
8429
8730
  // src/index.ts
8430
- import fs21 from "fs/promises";
8731
+ import fs20 from "fs/promises";
8431
8732
  import { createRequire as createRequire2 } from "module";
8432
8733
  var _require2 = createRequire2(import.meta.url);
8433
8734
  var VERSION = _require2("../package.json").version;
8434
- var modelChoices = MODEL_IDS.join(", ");
8435
8735
  if (process.argv[2] === "update") {
8436
8736
  await runUpdateCli(process.argv.slice(3));
8437
8737
  process.exit(process.exitCode ?? 0);
8438
8738
  }
8439
8739
  if (process.argv[2] === "ollama") {
8440
- const { runOllamaCli } = await import("./ollama-launch-2ASVER3S.js");
8740
+ const { runOllamaCli } = await import("./ollama-launch-3IKB2A3Z.js");
8441
8741
  const code = await runOllamaCli(process.argv.slice(3), process.cwd());
8442
8742
  process.exit(code);
8443
8743
  }
8444
- 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(
8744
+ if (process.argv[2] === "config") {
8745
+ const { runConfigCli } = await import("./config-set-5F4VK7IT.js");
8746
+ const code = await runConfigCli(process.argv.slice(3), process.cwd());
8747
+ process.exit(code);
8748
+ }
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(
8445
8750
  "--image <path>",
8446
8751
  "Attach an image (file path, URL, or data URL). Repeatable.",
8447
8752
  (val, prev) => prev ? [...prev, val] : [val],
@@ -8451,22 +8756,29 @@ var opts = program.opts();
8451
8756
  var promptArgs = program.args;
8452
8757
  function printByokProviderList() {
8453
8758
  const t = theme();
8454
- console.log(t.dim("\n BYOK providers (point --provider at any of these):\n"));
8455
- 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`;
8456
8766
  console.log(t.dim(header));
8457
8767
  console.log(t.dim(` ${"-".repeat(header.length - 2)}`));
8458
- for (const p of listByokProviders()) {
8459
- 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);
8460
8772
  const envDisplay = p.apiKeyEnv || "(none)";
8461
8773
  console.log(
8462
- ` ${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")}`
8463
8775
  );
8464
8776
  }
8465
8777
  console.log("");
8466
8778
  console.log(t.dim(" Use it like:"));
8467
8779
  console.log(t.dim(" export OPENROUTER_API_KEY=sk-or-..."));
8468
- console.log(t.dim(" notch --provider openrouter --model anthropic/claude-opus-4-7"));
8469
- 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"));
8470
8782
  console.log(t.dim(" notch --provider custom --base-url http://localhost:8000/v1 --model my-model"));
8471
8783
  console.log("");
8472
8784
  }
@@ -8483,13 +8795,14 @@ async function persistByokChoice(projectRoot, providerId, defaultModel) {
8483
8795
  const p = nodePath2.resolve(projectRoot, ".notch.json");
8484
8796
  let current = {};
8485
8797
  try {
8486
- const raw = await fs21.readFile(p, "utf-8");
8798
+ const raw = await fs20.readFile(p, "utf-8");
8487
8799
  current = JSON.parse(raw);
8488
8800
  } catch {
8489
8801
  }
8490
8802
  const idToPersist = providerId === "__custom__" ? "custom" : providerId;
8491
- current.byok = { provider: idToPersist, model: defaultModel };
8492
- await fs21.writeFile(p, JSON.stringify(current, null, 2) + "\n", "utf-8");
8803
+ current.provider = { provider: idToPersist, model: defaultModel };
8804
+ delete current.byok;
8805
+ await fs20.writeFile(p, JSON.stringify(current, null, 2) + "\n", "utf-8");
8493
8806
  } catch {
8494
8807
  }
8495
8808
  }
@@ -8502,14 +8815,12 @@ function interactiveModelPicker(activeModel) {
8502
8815
  return new Promise((resolve3) => {
8503
8816
  const t = theme();
8504
8817
  const rows = [];
8505
- rows.push({ kind: "notch-header" });
8506
- for (const id of MODEL_IDS) rows.push({ kind: "notch", id });
8507
8818
  rows.push({ kind: "byok-header" });
8508
8819
  for (const p of listByokProviders()) rows.push({ kind: "byok", provider: p });
8509
- 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);
8510
8821
  let cursor = selectableIndexes.find((i) => {
8511
8822
  const r = rows[i];
8512
- 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;
8513
8824
  }) ?? selectableIndexes[0] ?? 0;
8514
8825
  const rowCount = rows.length;
8515
8826
  const headerLines = 2;
@@ -8526,35 +8837,31 @@ function interactiveModelPicker(activeModel) {
8526
8837
  console.log(t.dim(" Select a model (\u2191\u2193 to move, Enter to select, Esc to cancel)\n"));
8527
8838
  for (let i = 0; i < rows.length; i++) {
8528
8839
  const row = rows[i];
8529
- if (row.kind === "notch-header") {
8530
- console.log(` ${t.dim("\u2500\u2500\u2500 Notch models (default) \u2500\u2500\u2500")}`);
8531
- continue;
8532
- }
8533
8840
  if (row.kind === "byok-header") {
8534
- console.log(` ${t.dim("\u2500\u2500\u2500 BYOK providers (your own API key) \u2500\u2500\u2500")}`);
8841
+ console.log(` ${t.dim("--- Providers ---")}`);
8535
8842
  continue;
8536
8843
  }
8537
8844
  const isSelected = i === cursor;
8538
8845
  const pointer = isSelected ? t.brand("\u276F") : " ";
8539
- if (row.kind === "notch") {
8540
- const info = MODEL_CATALOG[row.id];
8541
- const isCurrent = row.id === activeModel;
8542
- const dot = isCurrent ? t.success("\u25CF") : " ";
8543
- const label = isSelected ? t.bold(info.label) : t.dim(info.label);
8544
- const size = t.dim(info.size);
8545
- const gpu = t.dim(info.gpu);
8546
- const ctx = t.dim(`${(info.contextWindow / 1024).toFixed(0)}K`);
8547
- console.log(` ${pointer} ${dot} ${t.brand(row.id.replace("notch-", "").padEnd(12))} ${label.padEnd(20)} ${size.padEnd(6)} ${gpu.padEnd(12)} ${ctx}`);
8548
- } else {
8549
- const p = row.provider;
8550
- const isCurrent = typeof activeModel === "string" && isByokRef(activeModel) ? parseByokRef(activeModel).provider === p.id : false;
8551
- const dot = isCurrent ? t.success("\u25CF") : " ";
8552
- const keyPresent = p.apiKeyEnv ? process.env[p.apiKeyEnv] ? t.success("\u2713") : t.dim("\u2717") : t.dim("\u2013");
8553
- const label = isSelected ? t.bold(p.label) : t.dim(p.label);
8554
- const envDisplay = t.dim((p.apiKeyEnv || "local").padEnd(22));
8555
- const defModel = t.dim(p.defaultModel ? p.defaultModel.slice(0, 34) : "\u2014");
8556
- console.log(` ${pointer} ${dot} ${t.brand(p.id.padEnd(12))} ${label.padEnd(20)} ${envDisplay} ${keyPresent} ${defModel}`);
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 {
8858
+ }
8557
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}`);
8558
8865
  }
8559
8866
  };
8560
8867
  const render = (first) => {
@@ -8580,16 +8887,12 @@ function interactiveModelPicker(activeModel) {
8580
8887
  } else if (s === "\r" || s === "\n") {
8581
8888
  const row = rows[cursor];
8582
8889
  cleanup();
8583
- if (!row || row.kind !== "notch" && row.kind !== "byok") {
8890
+ if (!row || row.kind !== "byok") {
8584
8891
  resolve3(null);
8585
8892
  return;
8586
8893
  }
8587
- if (row.kind === "notch") {
8588
- resolve3({ kind: "notch", id: row.id });
8589
- } else {
8590
- const modelRef = `${row.provider.id}:${row.provider.defaultModel}`;
8591
- resolve3({ kind: "byok", provider: row.provider, modelRef });
8592
- }
8894
+ const modelRef = `${row.provider.id}/${row.provider.defaultModel}`;
8895
+ resolve3({ kind: "byok", provider: row.provider, modelRef });
8593
8896
  } else if (s === "\x1B" || s === "") {
8594
8897
  cleanup();
8595
8898
  resolve3(null);
@@ -8606,10 +8909,11 @@ function interactiveModelPicker(activeModel) {
8606
8909
  function printHelp() {
8607
8910
  console.log(chalk30.gray(`
8608
8911
  Commands:
8609
- /model \u2014 Show available models (Notch + BYOK)
8610
- /model <name> \u2014 Switch model: /model pyre OR /model openrouter:anthropic/claude-sonnet-4-6
8611
- /providers \u2014 List built-in BYOK providers and whether their keys are set
8612
- /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
8613
8917
  /undo \u2014 Undo last file changes
8614
8918
  /usage \u2014 Show token usage + context meter
8615
8919
  /cost \u2014 Show estimated session cost
@@ -8632,6 +8936,7 @@ function printHelp() {
8632
8936
  /memory search <q> \u2014 Search memories
8633
8937
  /memory clear \u2014 Delete all memories
8634
8938
  /permissions \u2014 Show current permission config
8939
+ /yolo \u2014 Toggle YOLO mode: auto-allow all tool calls (persists)
8635
8940
 
8636
8941
  Ralph Wiggum Mode (autonomous):
8637
8942
  /ralph plan <goal> \u2014 Generate task plan for a goal
@@ -8708,12 +9013,62 @@ async function main() {
8708
9013
  const creds = await login();
8709
9014
  console.log(chalk30.green(`
8710
9015
  \u2713 Signed in as ${creds.email}`));
8711
- console.log(chalk30.gray(` API key stored in ${(await import("./auth-JQX6MHJG.js")).getCredentialsPath()}
8712
- `));
9016
+ console.log(chalk30.gray(` API key stored in ${(await import("./auth-UAMMP5IJ.js")).getCredentialsPath()}`));
9017
+ try {
9018
+ const { syncByokKeys } = await import("./auth-UAMMP5IJ.js");
9019
+ const synced = await syncByokKeys(creds.token);
9020
+ const providerCount = Object.keys(synced.keys).length;
9021
+ if (providerCount > 0) {
9022
+ console.log(chalk30.gray(
9023
+ ` Synced ${providerCount} provider key(s) from freesyntax.dev \u2192 ${(await import("./auth-UAMMP5IJ.js")).getByokSyncPath()}`
9024
+ ));
9025
+ } else {
9026
+ console.log(chalk30.gray(
9027
+ ` No provider keys on your profile yet \u2014 add them at freesyntax.dev/settings/keys then run ${chalk30.white("notch sync-keys")}.`
9028
+ ));
9029
+ }
9030
+ } catch (syncErr) {
9031
+ console.log(chalk30.yellow(
9032
+ ` (provider key sync skipped: ${syncErr.message.slice(0, 120)})`
9033
+ ));
9034
+ }
9035
+ console.log("");
8713
9036
  } catch (err) {
8714
9037
  spinner.stop();
8715
9038
  console.error(chalk30.red(`
8716
9039
  Login failed: ${err.message}
9040
+ `));
9041
+ process.exit(1);
9042
+ }
9043
+ return;
9044
+ }
9045
+ if (promptArgs[0] === "sync-keys") {
9046
+ const creds = await loadCredentials();
9047
+ if (!creds) {
9048
+ console.log(chalk30.gray("\n Not signed in. Run: notch login\n"));
9049
+ return;
9050
+ }
9051
+ const spinner = ora7("Pulling provider keys from freesyntax.dev...").start();
9052
+ try {
9053
+ const { syncByokKeys, getByokSyncPath } = await import("./auth-UAMMP5IJ.js");
9054
+ const synced = await syncByokKeys(creds.token);
9055
+ spinner.stop();
9056
+ const providers = Object.keys(synced.keys);
9057
+ if (providers.length === 0) {
9058
+ console.log(chalk30.gray(`
9059
+ No provider keys on your profile yet.`));
9060
+ console.log(chalk30.gray(` Add them at ${chalk30.white("https://freesyntax.dev/settings/keys")} then rerun.
9061
+ `));
9062
+ } else {
9063
+ console.log(chalk30.green(`
9064
+ \u2713 Synced ${providers.length} provider key(s): ${providers.join(", ")}`));
9065
+ console.log(chalk30.gray(` Cached at ${getByokSyncPath()}
9066
+ `));
9067
+ }
9068
+ } catch (err) {
9069
+ spinner.stop();
9070
+ console.error(chalk30.red(`
9071
+ Key sync failed: ${err.message}
8717
9072
  `));
8718
9073
  process.exit(1);
8719
9074
  }
@@ -8746,7 +9101,7 @@ async function main() {
8746
9101
  return;
8747
9102
  }
8748
9103
  if (promptArgs[0] === "mcp-serve" || promptArgs[0] === "mcp-server") {
8749
- const { runMcpServer } = await import("./server-7UQKCB2Z.js");
9104
+ const { runMcpServer } = await import("./server-GMF4WV67.js");
8750
9105
  await runMcpServer({
8751
9106
  cwd: opts.cwd ?? process.cwd(),
8752
9107
  version: VERSION,
@@ -8785,7 +9140,7 @@ async function main() {
8785
9140
  const providerId = opts.provider === "custom" ? "__custom__" : opts.provider;
8786
9141
  const byokInfo = findByokProvider(providerId);
8787
9142
  if (!byokInfo) {
8788
- console.error(chalk30.red(` Unknown BYOK provider: ${opts.provider}`));
9143
+ console.error(chalk30.red(` Unknown provider: ${opts.provider}`));
8789
9144
  console.error(chalk30.gray(` Run notch --list-providers to see built-ins, or pass --provider custom --base-url <url>.`));
8790
9145
  process.exit(1);
8791
9146
  }
@@ -8802,7 +9157,7 @@ async function main() {
8802
9157
  } else if (isByokRef(opts.model)) {
8803
9158
  const { provider } = parseByokRef(opts.model);
8804
9159
  if (!findByokProvider(provider)) {
8805
- console.error(chalk30.red(` Unknown BYOK provider in ref: ${provider}`));
9160
+ console.error(chalk30.red(` Unknown provider in model ref: ${provider}`));
8806
9161
  console.error(chalk30.gray(` Run notch --list-providers to see built-ins.`));
8807
9162
  process.exit(1);
8808
9163
  }
@@ -8810,8 +9165,8 @@ async function main() {
8810
9165
  config.models.chat.byokProvider = void 0;
8811
9166
  } else {
8812
9167
  console.error(chalk30.red(` Unknown model: ${opts.model}`));
8813
- console.error(chalk30.gray(` Notch models: ${modelChoices}`));
8814
- 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."));
8815
9170
  process.exit(1);
8816
9171
  }
8817
9172
  }
@@ -8865,7 +9220,7 @@ async function main() {
8865
9220
  if (err instanceof ByokMissingApiKeyError) {
8866
9221
  printWordmark(VERSION);
8867
9222
  const p = err.provider;
8868
- console.log(` To use ${p.label} (BYOK) you need an API key.`);
9223
+ console.log(` To use ${p.label} you need an API key.`);
8869
9224
  console.log("");
8870
9225
  if (p.apiKeyEnv) {
8871
9226
  console.log(" \x1B[1mOption 1:\x1B[0m Set the env var");
@@ -8892,9 +9247,9 @@ async function main() {
8892
9247
  console.log(" \x1B[1mOption 3:\x1B[0m Pass it inline");
8893
9248
  console.log(" \x1B[33m$ notch --api-key your-key-here\x1B[0m");
8894
9249
  console.log("");
8895
- 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, ...)");
8896
9251
  console.log(" \x1B[33m$ notch --list-providers\x1B[0m");
8897
- 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");
8898
9253
  console.log("");
8899
9254
  console.log(" Get your Notch key at: \x1B[4mhttps://freesyntax.dev/settings\x1B[0m");
8900
9255
  console.log("");
@@ -8905,7 +9260,7 @@ async function main() {
8905
9260
  const info = activeByok ? {
8906
9261
  id: activeModelId,
8907
9262
  label: `${activeByok.label}`,
8908
- size: "BYOK",
9263
+ size: "provider",
8909
9264
  gpu: activeByok.id,
8910
9265
  contextWindow: 128e3,
8911
9266
  maxOutputTokens: 8192,
@@ -8955,14 +9310,18 @@ async function main() {
8955
9310
  }).catch(() => {
8956
9311
  });
8957
9312
  }
9313
+ const startupPermissive = config.permissionMode === "trust" || !!config.autoConfirm || !!opts.yes;
8958
9314
  const hookTrustPrompt = async (commands) => {
9315
+ if (startupPermissive) return true;
9316
+ if (!process.stdin.isTTY) return true;
8959
9317
  console.warn(chalk30.yellow("\n\u26A0 This project contains hooks in .notch.json that will run shell commands:"));
8960
9318
  commands.forEach((cmd) => console.warn(chalk30.gray(` \u2022 ${cmd}`)));
8961
9319
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
8962
9320
  return new Promise((resolve3) => {
8963
- rl2.question(chalk30.yellow("\nAllow these hooks for this project? [y/N] "), (answer) => {
9321
+ rl2.question(chalk30.yellow("\nAllow these hooks for this project? [Y/n] "), (answer) => {
8964
9322
  rl2.close();
8965
- resolve3(answer.trim().toLowerCase() === "y");
9323
+ const norm = answer.trim().toLowerCase();
9324
+ resolve3(norm !== "n" && norm !== "no");
8966
9325
  });
8967
9326
  });
8968
9327
  };
@@ -8981,7 +9340,6 @@ async function main() {
8981
9340
  spinner.warn("Could not build repo map");
8982
9341
  }
8983
9342
  }
8984
- const baseSystemPrompt = await buildSystemPrompt(config.projectRoot, activeModelId);
8985
9343
  let outputSchema = null;
8986
9344
  if (opts.outputSchema) {
8987
9345
  try {
@@ -8996,19 +9354,6 @@ async function main() {
8996
9354
  }
8997
9355
  }
8998
9356
  const coordinatorMode = !!opts.coordinator || isCoordinatorModeEnv();
8999
- const systemPrompt = coordinatorMode ? [
9000
- COORDINATOR_SYSTEM_PROMPT,
9001
- repoMapStr ? `
9002
- ## Repository Map
9003
- ${repoMapStr}` : "",
9004
- outputSchema ? schemaInstructions(outputSchema) : ""
9005
- ].join("") : [
9006
- baseSystemPrompt,
9007
- repoMapStr ? `
9008
- ## Repository Map
9009
- ${repoMapStr}` : "",
9010
- outputSchema ? schemaInstructions(outputSchema) : ""
9011
- ].join("");
9012
9357
  if (coordinatorMode && !jsonMode) {
9013
9358
  console.log(
9014
9359
  chalk30.green(
@@ -9026,7 +9371,7 @@ ${repoMapStr}` : "",
9026
9371
  const permissionSessionId = `s_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
9027
9372
  const checkpoints = new CheckpointManager();
9028
9373
  const usage2 = new UsageTracker();
9029
- const { OllamaCloudUsageTracker } = await import("./ollama-usage-2WPCZJJI.js");
9374
+ const { OllamaCloudUsageTracker } = await import("./ollama-usage-3PROM2WC.js");
9030
9375
  const cloudUsage = new OllamaCloudUsageTracker();
9031
9376
  let sessionId;
9032
9377
  let activePlan = null;
@@ -9034,19 +9379,11 @@ ${repoMapStr}` : "",
9034
9379
  const branches = /* @__PURE__ */ new Map();
9035
9380
  let currentBranch = "main";
9036
9381
  const costTracker = new CostTracker();
9037
- const mcpClients = [];
9038
9382
  try {
9039
- const configRaw = await fs21.readFile(nodePath2.resolve(config.projectRoot, ".notch.json"), "utf-8").catch(() => "{}");
9040
- const mcpConfigs = parseMCPConfig(JSON.parse(configRaw));
9041
- for (const [name, mcpConfig] of Object.entries(mcpConfigs)) {
9042
- try {
9043
- const client = new MCPClient(mcpConfig, name);
9044
- await client.connect();
9045
- mcpClients.push(client);
9046
- console.log(chalk30.green(` MCP: Connected to ${name} (${client.tools.length} tools)`));
9047
- } catch (err) {
9048
- console.log(chalk30.yellow(` MCP: Could not connect to ${name}: ${err.message}`));
9049
- }
9383
+ const configRaw = await fs20.readFile(nodePath2.resolve(config.projectRoot, ".notch.json"), "utf-8").catch(() => "{}");
9384
+ const toolCount = await initMCPServers(JSON.parse(configRaw));
9385
+ if (toolCount > 0) {
9386
+ console.log(chalk30.green(` MCP: Connected (${toolCount} tools)`));
9050
9387
  }
9051
9388
  } catch {
9052
9389
  }
@@ -9064,13 +9401,16 @@ ${repoMapStr}` : "",
9064
9401
  if (opts.guardian) {
9065
9402
  try {
9066
9403
  guardianModel = resolveModel({
9067
- model: "notch-solace-lite",
9404
+ model: process.env.NOTCH_GUARDIAN_MODEL ?? config.models.chat.model,
9068
9405
  apiKey: config.models.chat.apiKey,
9069
9406
  baseUrl: process.env.NOTCH_GUARDIAN_BASE_URL,
9070
- 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
9071
9411
  });
9072
9412
  if (!jsonMode) {
9073
- 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})`));
9074
9414
  }
9075
9415
  } catch (err) {
9076
9416
  if (!jsonMode) {
@@ -9079,15 +9419,18 @@ ${repoMapStr}` : "",
9079
9419
  guardianModel = void 0;
9080
9420
  }
9081
9421
  }
9422
+ const isPermissive = () => config.permissionMode === "trust" || config.autoConfirm;
9082
9423
  const toolCtx = {
9083
9424
  cwd: config.projectRoot,
9084
- requireConfirm: config.permissionMode !== "trust" && !config.autoConfirm,
9425
+ requireConfirm: !isPermissive(),
9085
9426
  confirm: async (message) => {
9427
+ if (isPermissive()) return true;
9086
9428
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
9087
9429
  return new Promise((resolve3) => {
9088
- rl2.question(`${message} (y/N) `, (answer) => {
9430
+ rl2.question(`${message} (Y/n) `, (answer) => {
9089
9431
  rl2.close();
9090
- resolve3(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
9432
+ const norm = answer.trim().toLowerCase();
9433
+ resolve3(norm !== "n" && norm !== "no");
9091
9434
  });
9092
9435
  });
9093
9436
  },
@@ -9112,6 +9455,28 @@ ${repoMapStr}` : "",
9112
9455
  }
9113
9456
  }
9114
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("");
9115
9480
  await toolCtx.runHook?.("session-start", {});
9116
9481
  const stopFileWatcher = startFileWatcher(config.projectRoot, hookConfig, (event, results) => {
9117
9482
  for (const r of results) {
@@ -9183,9 +9548,10 @@ Analyze the above input.`;
9183
9548
  attachments.push(res);
9184
9549
  }
9185
9550
  if (attachments.length > 0 && !modelSupportsImages(activeModelId)) {
9186
- const msg = `Selected model "${activeModelId}" may not support images. Switch with --model or --provider.`;
9187
- if (jsonMode) events.emit({ type: "warning", message: msg });
9188
- 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);
9189
9555
  }
9190
9556
  if (attachments.length > 0) {
9191
9557
  const imageBlocks = attachments.map(imageToContentBlock);
@@ -9305,7 +9671,7 @@ Analyze the above input.`;
9305
9671
  }
9306
9672
  if (opts.outputLastMessage) {
9307
9673
  try {
9308
- await fs21.writeFile(opts.outputLastMessage, response.text, "utf-8");
9674
+ await fs20.writeFile(opts.outputLastMessage, response.text, "utf-8");
9309
9675
  } catch (err) {
9310
9676
  if (jsonMode) events.emit({ type: "error", message: `Failed to write --output-last-message: ${err.message}` });
9311
9677
  }
@@ -9393,11 +9759,9 @@ Analyze the above input.`;
9393
9759
  } catch {
9394
9760
  }
9395
9761
  }
9396
- for (const client of mcpClients) {
9397
- try {
9398
- await client.disconnect();
9399
- } catch {
9400
- }
9762
+ try {
9763
+ disconnectMCPServers();
9764
+ } catch {
9401
9765
  }
9402
9766
  slashMenu.cleanup();
9403
9767
  stopActiveLoop();
@@ -9424,24 +9788,6 @@ Analyze the above input.`;
9424
9788
  const picked = await interactiveModelPicker(activeModelId);
9425
9789
  if (!picked) {
9426
9790
  console.log(chalk30.gray(" Cancelled\n"));
9427
- } else if (picked.kind === "notch") {
9428
- if (picked.id === activeModelId) {
9429
- console.log(chalk30.gray(` Already using ${MODEL_CATALOG[picked.id].label}
9430
- `));
9431
- } else {
9432
- activeModelId = picked.id;
9433
- config.models.chat.model = activeModelId;
9434
- config.models.chat.byokProvider = void 0;
9435
- try {
9436
- model = resolveModel(config.models.chat);
9437
- const switchedInfo = MODEL_CATALOG[picked.id];
9438
- console.log(chalk30.green(` \u2713 Switched to ${switchedInfo.label} (${switchedInfo.id})
9439
- `));
9440
- } catch (e) {
9441
- console.log(chalk30.red(` Failed to switch: ${e.message}
9442
- `));
9443
- }
9444
- }
9445
9791
  } else {
9446
9792
  activeModelId = picked.modelRef;
9447
9793
  config.models.chat.model = picked.modelRef;
@@ -9465,12 +9811,12 @@ Analyze the above input.`;
9465
9811
  rl.prompt();
9466
9812
  return;
9467
9813
  }
9468
- if (input.startsWith("/model ")) {
9814
+ if (input.startsWith("/model ") && !input.startsWith("/model download")) {
9469
9815
  const arg = input.replace("/model ", "").trim();
9470
9816
  if (isByokRef(arg)) {
9471
9817
  const { provider } = parseByokRef(arg);
9472
9818
  if (!findByokProvider(provider)) {
9473
- console.log(chalk30.red(` Unknown BYOK provider: ${provider}`));
9819
+ console.log(chalk30.red(` Unknown provider: ${provider}`));
9474
9820
  console.log(chalk30.gray(` Run /providers to list built-ins.`));
9475
9821
  rl.prompt();
9476
9822
  return;
@@ -9489,6 +9835,29 @@ Analyze the above input.`;
9489
9835
  `));
9490
9836
  } else {
9491
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}
9492
9861
  `));
9493
9862
  }
9494
9863
  }
@@ -9501,8 +9870,8 @@ Analyze the above input.`;
9501
9870
  }
9502
9871
  if (!isValidModel(newModel)) {
9503
9872
  console.log(chalk30.red(` Unknown model: ${arg}`));
9504
- console.log(chalk30.gray(` Notch models: ${modelChoices}`));
9505
- 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."));
9506
9875
  rl.prompt();
9507
9876
  return;
9508
9877
  }
@@ -9516,6 +9885,60 @@ Analyze the above input.`;
9516
9885
  rl.prompt();
9517
9886
  return;
9518
9887
  }
9888
+ if (input.startsWith("/model download")) {
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"));
9891
+ rl.prompt();
9892
+ return;
9893
+ }
9894
+ if (input === "/downloads") {
9895
+ const { listDownloads } = await import("./model-download-KCQJCEPW.js");
9896
+ const active = listDownloads();
9897
+ if (active.length === 0) {
9898
+ console.log(chalk30.gray(" No local downloads started this session.\n"));
9899
+ } else {
9900
+ console.log(chalk30.gray("\n Model State Last line"));
9901
+ for (const h of active) {
9902
+ const state = h.result == null ? chalk30.yellow("running") : h.result.code === 0 ? chalk30.green("done") : chalk30.red("error");
9903
+ const info2 = MODEL_CATALOG[h.modelId];
9904
+ const label = (info2?.label ?? h.modelId).padEnd(18);
9905
+ const tail2 = (h.lastLine || "").slice(0, 80);
9906
+ console.log(` ${label} ${state.padEnd(20)} ${chalk30.gray(tail2)}`);
9907
+ }
9908
+ console.log("");
9909
+ }
9910
+ rl.prompt();
9911
+ return;
9912
+ }
9913
+ if (input === "/sync-keys") {
9914
+ const { loadCredentials: loadCredentials2, syncByokKeys, getByokSyncPath } = await import("./auth-UAMMP5IJ.js");
9915
+ const creds = await loadCredentials2();
9916
+ if (!creds) {
9917
+ console.log(chalk30.gray(" Not signed in. Run: notch login\n"));
9918
+ rl.prompt();
9919
+ return;
9920
+ }
9921
+ const spinner2 = ora7("Pulling provider keys from freesyntax.dev...").start();
9922
+ try {
9923
+ const synced = await syncByokKeys(creds.token);
9924
+ spinner2.stop();
9925
+ const providers = Object.keys(synced.keys);
9926
+ if (providers.length === 0) {
9927
+ console.log(chalk30.gray(` No provider keys on your profile yet. Add them at https://freesyntax.dev/settings/keys.
9928
+ `));
9929
+ } else {
9930
+ console.log(chalk30.green(` \u2713 Synced ${providers.length} key(s): ${providers.join(", ")}`));
9931
+ console.log(chalk30.gray(` Cached at ${getByokSyncPath()}
9932
+ `));
9933
+ }
9934
+ } catch (err) {
9935
+ spinner2.stop();
9936
+ console.log(chalk30.red(` Sync failed: ${err.message}
9937
+ `));
9938
+ }
9939
+ rl.prompt();
9940
+ return;
9941
+ }
9519
9942
  if (input === "/providers") {
9520
9943
  printByokProviderList();
9521
9944
  rl.prompt();
@@ -9586,7 +10009,7 @@ Analyze the above input.`;
9586
10009
  return;
9587
10010
  }
9588
10011
  if (input === "/compact") {
9589
- const { autoCompress: autoCompress2 } = await import("./compression-SQAIQ2UU.js");
10012
+ const { autoCompress: autoCompress2 } = await import("./compression-YJLWEHCC.js");
9590
10013
  const before = messages.length;
9591
10014
  const compressed = await autoCompress2(messages, model, activeContextWindow(config.models.chat));
9592
10015
  messages.length = 0;
@@ -9889,6 +10312,29 @@ Analyze the above input.`;
9889
10312
  }
9890
10313
  if (input === "/permissions") {
9891
10314
  console.log(formatPermissions(permissions));
10315
+ const mode = config.permissionMode === "trust" ? "trust (YOLO)" : config.permissionMode;
10316
+ console.log(chalk30.gray(` Mode: ${mode}${config.autoConfirm ? " (autoConfirm)" : ""}`));
10317
+ console.log(chalk30.gray(" Toggle YOLO with /yolo (persists to .notch.json)."));
10318
+ console.log("");
10319
+ rl.prompt();
10320
+ return;
10321
+ }
10322
+ if (input === "/yolo" || input === "/yes" || input === "/trust" || input === "/strict" || input === "/autoaccept" || input === "/auto") {
10323
+ const turnOn = input === "/yolo" ? config.permissionMode !== "trust" : input === "/yes" || input === "/trust" || input === "/autoaccept" || input === "/auto";
10324
+ config.permissionMode = turnOn ? "trust" : "auto";
10325
+ config.autoConfirm = turnOn;
10326
+ toolCtx.checkPermission = turnOn ? () => "allow" : (toolName, args) => checkPermission(permissions, toolName, args);
10327
+ toolCtx.requireConfirm = !turnOn;
10328
+ toolCtx.autoConfirm = turnOn;
10329
+ try {
10330
+ await persistConfigPatch(config.projectRoot, {
10331
+ permissionMode: config.permissionMode,
10332
+ autoConfirm: config.autoConfirm || void 0
10333
+ });
10334
+ console.log(turnOn ? chalk30.red(` \u{1F525} YOLO mode ON \u2014 all tool calls auto-allowed, no prompts. Saved to .notch.json.`) : chalk30.cyan(` Strict mode ON \u2014 prompts restored. Saved to .notch.json.`));
10335
+ } catch (err) {
10336
+ console.log(chalk30.yellow(` Mode set for this session but persist failed: ${err?.message ?? err}`));
10337
+ }
9892
10338
  console.log("");
9893
10339
  rl.prompt();
9894
10340
  return;
@@ -10082,9 +10528,10 @@ Analyze the above input.`;
10082
10528
  if (replAttachments.length > 0 && !modelSupportsImages(activeModelId)) {
10083
10529
  console.warn(
10084
10530
  chalk30.yellow(
10085
- ` \u26A0 ${activeModelId} may not support images. Switch with /model.`
10531
+ ` \u26A0 ${activeModelId} does not support image attachments. Switch with /model; skipping image(s).`
10086
10532
  )
10087
10533
  );
10534
+ replAttachments.length = 0;
10088
10535
  }
10089
10536
  const textForRefs = imgRefs.cleanedText || (replAttachments.length > 0 ? "Describe the attached image(s)." : "");
10090
10537
  const { cleanInput, references } = await resolveReferences(textForRefs, config.projectRoot);
@@ -10093,6 +10540,10 @@ Analyze the above input.`;
10093
10540
  if (references.length > 0) {
10094
10541
  console.log(chalk30.gray(` Injected ${references.length} reference(s)`));
10095
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
+ }
10096
10547
  if (replAttachments.length > 0) {
10097
10548
  const imageBlocks = replAttachments.map(imageToContentBlock);
10098
10549
  messages.push({
@@ -10268,7 +10719,6 @@ async function handleRalphSubcommand(args, cliOpts) {
10268
10719
  const config = await loadConfig(cliOpts.cwd ? { projectRoot: cliOpts.cwd } : {});
10269
10720
  if (cliOpts.model) config.models.chat.model = cliOpts.model;
10270
10721
  const model = resolveModel(config.models.chat);
10271
- const systemPrompt = await buildSystemPrompt(config.projectRoot, config.models.chat.model);
10272
10722
  const toolCtx = {
10273
10723
  cwd: config.projectRoot,
10274
10724
  requireConfirm: false,