savant-code 0.0.20 → 0.0.22

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 (3) hide show
  1. package/README.md +37 -13
  2. package/launcher.js +138 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -45,13 +45,17 @@ Or select one of the supported gateway providers directly:
45
45
 
46
46
  | Provider | Selection | Environment variable | Notes |
47
47
  | --- | --- | --- | --- |
48
- | Ollama | Automatic detection | `OLLAMA_HOST` (optional) | Local inference; no API key required |
49
- | OpenCode Go | `/provider opencode-go` | `OPENCODE_GO_API_KEY` | Hosted gateway; MiMo 2.5 is the default model |
50
- | TokenRouter | `/provider tokenrouter` | `TOKENROUTER_API_KEY` | Multi-provider gateway |
51
- | NVIDIA NIM | `/provider nvidia` | `NVIDIA_API_KEY` | NVIDIA-hosted inference |
52
- | CommandCode | `/provider commandcode` | `COMMAND_CODE_API_KEY` | OpenAI-compatible hosted inference |
53
- | OpenRouter direct | `/provider openrouter` or `DIRECT_PROVIDER=openrouter` | `OR_MASTER_KEY`, `OPENROUTER_API_KEY`, or `INFERENCE_API_KEY` | Direct mode without the Savant backend |
54
- | Custom endpoint | Environment configuration | `INFERENCE_BASE_URL`, `INFERENCE_API_KEY` | Advanced OpenAI-compatible endpoint |
48
+ <!-- GENERATED:provider-table-start -->
49
+ | OpenRouter | `/provider openrouter` or `DIRECT_PROVIDER=openrouter` | `OR_MASTER_KEY`, `OPENROUTER_API_KEY`, or `INFERENCE_API_KEY` | Default provider; free tier (`openrouter/free`) is the boot default; direct mode without the Savant backend |
50
+ | TokenRouter | `/provider tokenrouter` or `DIRECT_PROVIDER=tokenrouter` | `TOKENROUTER_API_KEY` | Multi-provider gateway |
51
+ | NVIDIA NIM | `/provider nvidia` or `DIRECT_PROVIDER=nvidia` | `NVIDIA_API_KEY` | NVIDIA-hosted inference |
52
+ | OpenCode Go | `/provider opencode-go` or `DIRECT_PROVIDER=opencode-go` | `OPENCODE_GO_API_KEY` | Hosted gateway (dual-protocol) |
53
+ | Cloudflare | Environment configuration | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` | Env-only not in the `/provider` picker; requires the account id too |
54
+ | CommandCode | `/provider commandcode` or `DIRECT_PROVIDER=commandcode` | `COMMAND_CODE_API_KEY` | OpenAI-compatible hosted inference (dual-protocol) |
55
+ | Ollama | Automatic detection | `OLLAMA_HOST` (optional) | Local inference; no API key required |
56
+ | TokenHarbor | `/provider tokenharbor` or `DIRECT_PROVIDER=tokenharbor` | `TOKENHARBOR_API_KEY` | OpenAI-compatible hosted gateway |
57
+ | Custom endpoint | Environment configuration | `INFERENCE_BASE_URL`, `INFERENCE_API_KEY` | Advanced OpenAI-compatible endpoint |
58
+ <!-- GENERATED:provider-table-end -->
55
59
 
56
60
  The interactive key prompt is masked. Saved provider credentials are stored in the user configuration directory and are not added to chat history:
57
61
 
@@ -63,6 +67,7 @@ Shell environment variables take precedence over saved credentials. Configure on
63
67
  ```powershell
64
68
  # PowerShell — choose one hosted gateway
65
69
  $env:OPENCODE_GO_API_KEY = "your-key"
70
+ # $env:TOKENHARBOR_API_KEY = "your-key"
66
71
  # $env:TOKENROUTER_API_KEY = "your-key"
67
72
  # $env:NVIDIA_API_KEY = "your-key"
68
73
  # $env:COMMAND_CODE_API_KEY = "your-key"
@@ -72,6 +77,7 @@ savant-code
72
77
  ```cmd
73
78
  :: Windows Command Prompt — choose one hosted gateway
74
79
  set OPENCODE_GO_API_KEY=your-key
80
+ :: set TOKENHARBOR_API_KEY=your-key
75
81
  :: set TOKENROUTER_API_KEY=your-key
76
82
  :: set NVIDIA_API_KEY=your-key
77
83
  :: set COMMAND_CODE_API_KEY=your-key
@@ -81,6 +87,7 @@ savant-code
81
87
  ```bash
82
88
  # macOS/Linux — choose one hosted gateway
83
89
  export OPENCODE_GO_API_KEY="your-key"
90
+ # export TOKENHARBOR_API_KEY="your-key"
84
91
  # export TOKENROUTER_API_KEY="your-key"
85
92
  # export NVIDIA_API_KEY="your-key"
86
93
  # export COMMAND_CODE_API_KEY="your-key"
@@ -111,7 +118,7 @@ The following is a public template containing dummy values only. Copy it to `.en
111
118
  NEXT_PUBLIC_CB_ENVIRONMENT=dev
112
119
  NEXT_PUBLIC_WEB_PORT=3000
113
120
  NEXT_PUBLIC_SAVANT_CODE_APP_URL=http://localhost:3000
114
- # NEXT_PUBLIC_FREEBUFF_APP_URL=http://localhost:3001
121
+ # NEXT_PUBLIC_SAVANT_FREE_APP_URL=http://localhost:3001
115
122
 
116
123
  # Analytics, support, and billing placeholders
117
124
  NEXT_PUBLIC_POSTHOG_API_KEY=phc_dummy_replace_me
@@ -131,6 +138,7 @@ OR_MASTER_KEY=dummy-or-master-key-replace-me
131
138
 
132
139
  # Supported hosted gateways
133
140
  OPENCODE_GO_API_KEY=dummy-opencode-go-key-replace-me
141
+ TOKENHARBOR_API_KEY=dummy-tokenharbor-key-replace-me
134
142
  TOKENROUTER_API_KEY=dummy-tokenrouter-key-replace-me
135
143
  NVIDIA_API_KEY=dummy-nvidia-key-replace-me
136
144
  COMMAND_CODE_API_KEY=dummy-commandcode-key-replace-me
@@ -162,7 +170,7 @@ Build release artifacts from a clean shell (no dev `NEXT_PUBLIC_*` exports, no `
162
170
 
163
171
  ## What Makes Savant-Code Different
164
172
 
165
- Savant-Code is a multi-agent system rather than a single model guessing at your code. Nine canonical ECHO roles coordinate with strict separation of duties:
173
+ Savant-Code is a multi-agent system rather than a single model guessing at your code. Ten canonical ECHO roles coordinate with strict separation of duties:
166
174
 
167
175
  | Agent | Responsibility |
168
176
  | --- | --- |
@@ -175,6 +183,7 @@ Savant-Code is a multi-agent system rather than a single model guessing at your
175
183
  | **Scout** | Explores files and code to gather context |
176
184
  | **Researcher** | Performs web search, documentation lookup, and multi-query `deep_research` |
177
185
  | **Scribe** | Captures session summaries and durable knowledge |
186
+ | **Adversary** | Meta-verification — refutes Verifier findings, re-audits unevidenced PASSes, resolves citations; verdicts override |
178
187
 
179
188
  Infrastructure helpers such as terminal execution, browser automation, and web/docs tool libraries support these roles; they are not additional roster members.
180
189
 
@@ -185,8 +194,10 @@ Every code change follows the ECHO Perfection Loop:
185
194
  1. **RED** — identify all failures and issues with evidence.
186
195
  2. **GREEN** — implement minimal, surgical changes from the converged FID.
187
196
  3. **AUDIT** — independently verify the implementation and call-graph reachability.
188
- 4. **SELF-CORRECT** — resolve audit findings and repeat verification when needed.
189
- 5. **COMPLETE** — record evidence, update tracking, and close the work item.
197
+ 4. **ADVERSARIAL** — the Adversary refutes Verifier findings, re-audits
198
+ unevidenced PASSes, and resolves citations; verdicts override.
199
+ 5. **SELF-CORRECT** — resolve audit findings and repeat verification when needed.
200
+ 6. **COMPLETE** — record evidence, update tracking, and close the work item.
190
201
 
191
202
  No code is written without a converged plan, and the implementing agent cannot serve as the final verifier.
192
203
 
@@ -194,7 +205,7 @@ No code is written without a converged plan, and the implementing agent cannot s
194
205
 
195
206
  ### Multi-agent orchestration
196
207
 
197
- - Nine canonical agents coordinate through ECHO with explicit separation of duties.
208
+ - Ten canonical agents coordinate through ECHO with explicit separation of duties.
198
209
  - Child agents receive only their authorized tool subset through strict allowlist filtering.
199
210
  - Parallel agent work supports exploration, research, implementation, and independent review.
200
211
  - FID-bound execution keeps implementation tied to an approved specification.
@@ -290,7 +301,7 @@ The package ships the CLI on top of shared runtime and SDK capabilities:
290
301
 
291
302
  ## Slash Command Reference
292
303
 
293
- Commands can be entered with `/`; aliases are shown in parentheses. Some commands are intentionally unavailable in Savant-Free builds.
304
+ Commands can be entered with `/`; aliases are shown in parentheses.
294
305
 
295
306
  | Command | Purpose |
296
307
  | --- | --- |
@@ -344,6 +355,19 @@ Commands can be entered with `/`; aliases are shown in parentheses. Some command
344
355
 
345
356
  > Review my recent changes and flag security issues, performance problems, and style violations.
346
357
 
358
+ **Headless / scripting (FID-2026-0806-011):**
359
+
360
+ ```bash
361
+ # Run a single prompt without the TUI and print the final answer to stdout
362
+ savant-code --print "summarize this repo"
363
+
364
+ # Pipe a prompt in — the CLI auto-enters headless mode
365
+ printf 'add a .gitignore\n' | savant-code
366
+ ```
367
+
368
+ Exit codes: `0` success, `1` error or timeout, `2` usage error.
369
+ `SAVANT_CODE_RUN_TIMEOUT_MS` (default 10 minutes) bounds hung runs.
370
+
347
371
  ## Troubleshooting
348
372
 
349
373
  ### Permission errors
package/launcher.js CHANGED
@@ -6,6 +6,7 @@ const http = require('http')
6
6
  const https = require('https')
7
7
  const os = require('os')
8
8
  const path = require('path')
9
+ const readline = require('readline')
9
10
  const { pipeline } = require('stream/promises')
10
11
  const zlib = require('zlib')
11
12
 
@@ -547,6 +548,77 @@ function createLauncher(productConfig) {
547
548
  )
548
549
  }
549
550
 
551
+ // FID-2026-0806-014: consent before apply. Auto-updates must not silently
552
+ // replace the running binary or kill the session (Windows locks the exe in
553
+ // place). The launcher stages the update in the background, records it in a
554
+ // pending marker, and offers to install it on the next launch — when it owns
555
+ // stdin and can ask. SAVANT_CODE_NO_AUTO_UPDATE=1 opts out entirely.
556
+ function getPendingUpdatePath() {
557
+ return path.join(CONFIG.configDir, `${packageName}-pending-update.json`)
558
+ }
559
+
560
+ function writePendingUpdateMarker(stagedBinary) {
561
+ try {
562
+ fs.mkdirSync(CONFIG.configDir, { recursive: true })
563
+ fs.writeFileSync(
564
+ getPendingUpdatePath(),
565
+ JSON.stringify(stagedBinary, null, 2),
566
+ )
567
+ } catch {
568
+ // Best effort; the update is simply not offered next launch.
569
+ }
570
+ }
571
+
572
+ function readPendingUpdateMarker() {
573
+ try {
574
+ const marker = JSON.parse(fs.readFileSync(getPendingUpdatePath(), 'utf8'))
575
+ if (
576
+ marker &&
577
+ typeof marker.version === 'string' &&
578
+ typeof marker.tempBinaryPath === 'string' &&
579
+ fs.existsSync(marker.tempBinaryPath)
580
+ ) {
581
+ return marker
582
+ }
583
+ // Stale marker without a staged binary — drop it.
584
+ try {
585
+ fs.rmSync(getPendingUpdatePath(), { force: true })
586
+ } catch {
587
+ // Best effort
588
+ }
589
+ return null
590
+ } catch {
591
+ return null
592
+ }
593
+ }
594
+
595
+ function clearPendingUpdateMarker() {
596
+ try {
597
+ fs.rmSync(getPendingUpdatePath(), { force: true })
598
+ } catch {
599
+ // Best effort
600
+ }
601
+ }
602
+
603
+ /** Ask the user a y/N question on stdin (launcher owns the terminal before
604
+ * the child spawns). Resolves false on anything that isn't y/yes. */
605
+ function askYesNo(question) {
606
+ return new Promise((resolve) => {
607
+ const rl = readline.createInterface({
608
+ input: process.stdin,
609
+ output: process.stdout,
610
+ })
611
+ const finish = (answer) => {
612
+ rl.close()
613
+ resolve(answer)
614
+ }
615
+ rl.question(question, (answer) => {
616
+ finish(/^y(es)?$/i.test(String(answer).trim()))
617
+ })
618
+ rl.on('SIGINT', () => finish(false))
619
+ })
620
+ }
621
+
550
622
  function getFileSize(filePath) {
551
623
  try {
552
624
  return fs.statSync(filePath).size
@@ -940,9 +1012,7 @@ function createLauncher(productConfig) {
940
1012
  })
941
1013
  }
942
1014
 
943
- async function checkForUpdates(runningProcess, exitListener) {
944
- let stoppedForUpdate = false
945
-
1015
+ async function checkForUpdates() {
946
1016
  try {
947
1017
  const currentVersion = getCurrentVersion()
948
1018
 
@@ -954,42 +1024,28 @@ function createLauncher(productConfig) {
954
1024
  currentVersion === null ||
955
1025
  compareVersions(currentVersion, latestVersion) < 0
956
1026
  ) {
1027
+ // FID-2026-0806-014: never install mid-session without consent.
1028
+ if (process.env.SAVANT_CODE_NO_AUTO_UPDATE) return
1029
+
1030
+ // Already staged for this exact version — leave the pending marker alone.
1031
+ const pending = readPendingUpdateMarker()
1032
+ if (pending && pending.version === latestVersion) return
1033
+
957
1034
  const stagedBinary = await stageBinary(
958
1035
  latestVersion,
959
1036
  getDownloadTargetKey(),
960
1037
  { quiet: true },
961
1038
  )
1039
+ writePendingUpdateMarker(stagedBinary)
962
1040
 
963
1041
  term.clearLine()
964
-
965
- runningProcess.removeListener('exit', exitListener)
966
- try {
967
- await stopRunningProcess(runningProcess)
968
- } catch (error) {
969
- runningProcess.on('exit', exitListener)
970
- throw error
971
- }
972
- stoppedForUpdate = true
973
-
974
- resetTerminal({ exitAlternateScreen: true })
975
- console.log(`Update available: ${currentVersion} → ${latestVersion}`)
976
-
977
- installStagedBinary(stagedBinary)
978
-
979
- const newChild = spawnInstalledBinary({ detached: false })
980
- attachExitHandler(newChild)
981
-
982
- return new Promise(() => {})
983
- }
984
- } catch (error) {
985
- if (stoppedForUpdate && fs.existsSync(CONFIG.binaryPath)) {
986
- console.error(
987
- `Update failed; restarting ${packageName} ${getCurrentVersion()}.`,
1042
+ term.writeLine(
1043
+ `Update available: ${currentVersion || 'unknown'} → ${latestVersion}. ` +
1044
+ 'It will be installed the next time you launch (or set ' +
1045
+ 'SAVANT_CODE_NO_AUTO_UPDATE=1 to skip).',
988
1046
  )
989
- const child = spawnInstalledBinary({ detached: false })
990
- attachExitHandler(child)
991
- return new Promise(() => {})
992
1047
  }
1048
+ } catch (error) {
993
1049
  try {
994
1050
  fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
995
1051
  } catch {
@@ -999,6 +1055,52 @@ function createLauncher(productConfig) {
999
1055
  }
1000
1056
  }
1001
1057
 
1058
+ /** Offer a pending update (staged last session) and install it on consent.
1059
+ * Runs before the child spawns, so the launcher owns stdin and the binary
1060
+ * is not locked by a running session. Returns true when an update was
1061
+ * installed. */
1062
+ async function applyPendingUpdateIfApproved() {
1063
+ if (process.env.SAVANT_CODE_NO_AUTO_UPDATE) {
1064
+ return false
1065
+ }
1066
+ const pending = readPendingUpdateMarker()
1067
+ if (!pending) {
1068
+ return false
1069
+ }
1070
+
1071
+ const currentVersion = getCurrentVersion()
1072
+ console.log(
1073
+ `Update available: ${currentVersion || 'unknown'} → ${pending.version}`,
1074
+ )
1075
+
1076
+ if (!process.stdin.isTTY) {
1077
+ console.log(
1078
+ 'Non-interactive launch — the update will be installed the next time ' +
1079
+ 'you run interactively.',
1080
+ )
1081
+ return false
1082
+ }
1083
+
1084
+ const approved = await askYesNo('Install now? [y/N] ')
1085
+ if (!approved) {
1086
+ console.log(
1087
+ 'Skipping update. Set SAVANT_CODE_NO_AUTO_UPDATE=1 to stop being asked.',
1088
+ )
1089
+ clearPendingUpdateMarker()
1090
+ return false
1091
+ }
1092
+
1093
+ try {
1094
+ installStagedBinary(pending)
1095
+ clearPendingUpdateMarker()
1096
+ return true
1097
+ } catch (error) {
1098
+ term.clearLine()
1099
+ printDownloadFailure(error)
1100
+ return false
1101
+ }
1102
+ }
1103
+
1002
1104
  function printCrashDiagnostics(code, signal) {
1003
1105
  // Windows NTSTATUS codes (unsigned DWORD)
1004
1106
  const unsignedCode = getUnsignedExitCode(code)
@@ -1213,11 +1315,15 @@ function createLauncher(productConfig) {
1213
1315
 
1214
1316
  await ensureBinaryExists()
1215
1317
 
1318
+ // FID-2026-0806-014: offer a staged update before the child takes over the
1319
+ // terminal. Consent-gated — never silent, never mid-session.
1320
+ await applyPendingUpdateIfApproved()
1321
+
1216
1322
  const child = spawnInstalledBinary()
1217
- const exitListener = attachExitHandler(child)
1323
+ attachExitHandler(child)
1218
1324
 
1219
1325
  setTimeout(() => {
1220
- checkForUpdates(child, exitListener)
1326
+ checkForUpdates()
1221
1327
  }, 100)
1222
1328
  }
1223
1329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "savant-code",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Multi-Agent AI Coding Assistant with ECHO Protocol",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Savant Code <founders@savant-code.com>",