savant-code 0.0.20 → 0.0.21

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 +29 -8
  2. package/launcher.js +138 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,11 +46,12 @@ Or select one of the supported gateway providers directly:
46
46
  | Provider | Selection | Environment variable | Notes |
47
47
  | --- | --- | --- | --- |
48
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 |
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
+ | OpenCode Go | `/provider opencode-go` | `OPENCODE_GO_API_KEY` | Hosted gateway |
50
51
  | TokenRouter | `/provider tokenrouter` | `TOKENROUTER_API_KEY` | Multi-provider gateway |
52
+ | TokenHarbor | `/provider tokenharbor` | `TOKENHARBOR_API_KEY` | OpenAI-compatible hosted gateway at `https://tokenharbor.ai/v1` |
51
53
  | NVIDIA NIM | `/provider nvidia` | `NVIDIA_API_KEY` | NVIDIA-hosted inference |
52
54
  | 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
55
  | Custom endpoint | Environment configuration | `INFERENCE_BASE_URL`, `INFERENCE_API_KEY` | Advanced OpenAI-compatible endpoint |
55
56
 
56
57
  The interactive key prompt is masked. Saved provider credentials are stored in the user configuration directory and are not added to chat history:
@@ -63,6 +64,7 @@ Shell environment variables take precedence over saved credentials. Configure on
63
64
  ```powershell
64
65
  # PowerShell — choose one hosted gateway
65
66
  $env:OPENCODE_GO_API_KEY = "your-key"
67
+ # $env:TOKENHARBOR_API_KEY = "your-key"
66
68
  # $env:TOKENROUTER_API_KEY = "your-key"
67
69
  # $env:NVIDIA_API_KEY = "your-key"
68
70
  # $env:COMMAND_CODE_API_KEY = "your-key"
@@ -72,6 +74,7 @@ savant-code
72
74
  ```cmd
73
75
  :: Windows Command Prompt — choose one hosted gateway
74
76
  set OPENCODE_GO_API_KEY=your-key
77
+ :: set TOKENHARBOR_API_KEY=your-key
75
78
  :: set TOKENROUTER_API_KEY=your-key
76
79
  :: set NVIDIA_API_KEY=your-key
77
80
  :: set COMMAND_CODE_API_KEY=your-key
@@ -81,6 +84,7 @@ savant-code
81
84
  ```bash
82
85
  # macOS/Linux — choose one hosted gateway
83
86
  export OPENCODE_GO_API_KEY="your-key"
87
+ # export TOKENHARBOR_API_KEY="your-key"
84
88
  # export TOKENROUTER_API_KEY="your-key"
85
89
  # export NVIDIA_API_KEY="your-key"
86
90
  # export COMMAND_CODE_API_KEY="your-key"
@@ -111,7 +115,7 @@ The following is a public template containing dummy values only. Copy it to `.en
111
115
  NEXT_PUBLIC_CB_ENVIRONMENT=dev
112
116
  NEXT_PUBLIC_WEB_PORT=3000
113
117
  NEXT_PUBLIC_SAVANT_CODE_APP_URL=http://localhost:3000
114
- # NEXT_PUBLIC_FREEBUFF_APP_URL=http://localhost:3001
118
+ # NEXT_PUBLIC_SAVANT_FREE_APP_URL=http://localhost:3001
115
119
 
116
120
  # Analytics, support, and billing placeholders
117
121
  NEXT_PUBLIC_POSTHOG_API_KEY=phc_dummy_replace_me
@@ -131,6 +135,7 @@ OR_MASTER_KEY=dummy-or-master-key-replace-me
131
135
 
132
136
  # Supported hosted gateways
133
137
  OPENCODE_GO_API_KEY=dummy-opencode-go-key-replace-me
138
+ TOKENHARBOR_API_KEY=dummy-tokenharbor-key-replace-me
134
139
  TOKENROUTER_API_KEY=dummy-tokenrouter-key-replace-me
135
140
  NVIDIA_API_KEY=dummy-nvidia-key-replace-me
136
141
  COMMAND_CODE_API_KEY=dummy-commandcode-key-replace-me
@@ -162,7 +167,7 @@ Build release artifacts from a clean shell (no dev `NEXT_PUBLIC_*` exports, no `
162
167
 
163
168
  ## What Makes Savant-Code Different
164
169
 
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:
170
+ 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
171
 
167
172
  | Agent | Responsibility |
168
173
  | --- | --- |
@@ -175,6 +180,7 @@ Savant-Code is a multi-agent system rather than a single model guessing at your
175
180
  | **Scout** | Explores files and code to gather context |
176
181
  | **Researcher** | Performs web search, documentation lookup, and multi-query `deep_research` |
177
182
  | **Scribe** | Captures session summaries and durable knowledge |
183
+ | **Adversary** | Meta-verification — refutes Verifier findings, re-audits unevidenced PASSes, resolves citations; verdicts override |
178
184
 
179
185
  Infrastructure helpers such as terminal execution, browser automation, and web/docs tool libraries support these roles; they are not additional roster members.
180
186
 
@@ -185,8 +191,10 @@ Every code change follows the ECHO Perfection Loop:
185
191
  1. **RED** — identify all failures and issues with evidence.
186
192
  2. **GREEN** — implement minimal, surgical changes from the converged FID.
187
193
  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.
194
+ 4. **ADVERSARIAL** — the Adversary refutes Verifier findings, re-audits
195
+ unevidenced PASSes, and resolves citations; verdicts override.
196
+ 5. **SELF-CORRECT** — resolve audit findings and repeat verification when needed.
197
+ 6. **COMPLETE** — record evidence, update tracking, and close the work item.
190
198
 
191
199
  No code is written without a converged plan, and the implementing agent cannot serve as the final verifier.
192
200
 
@@ -194,7 +202,7 @@ No code is written without a converged plan, and the implementing agent cannot s
194
202
 
195
203
  ### Multi-agent orchestration
196
204
 
197
- - Nine canonical agents coordinate through ECHO with explicit separation of duties.
205
+ - Ten canonical agents coordinate through ECHO with explicit separation of duties.
198
206
  - Child agents receive only their authorized tool subset through strict allowlist filtering.
199
207
  - Parallel agent work supports exploration, research, implementation, and independent review.
200
208
  - FID-bound execution keeps implementation tied to an approved specification.
@@ -290,7 +298,7 @@ The package ships the CLI on top of shared runtime and SDK capabilities:
290
298
 
291
299
  ## Slash Command Reference
292
300
 
293
- Commands can be entered with `/`; aliases are shown in parentheses. Some commands are intentionally unavailable in Savant-Free builds.
301
+ Commands can be entered with `/`; aliases are shown in parentheses.
294
302
 
295
303
  | Command | Purpose |
296
304
  | --- | --- |
@@ -344,6 +352,19 @@ Commands can be entered with `/`; aliases are shown in parentheses. Some command
344
352
 
345
353
  > Review my recent changes and flag security issues, performance problems, and style violations.
346
354
 
355
+ **Headless / scripting (FID-2026-0806-011):**
356
+
357
+ ```bash
358
+ # Run a single prompt without the TUI and print the final answer to stdout
359
+ savant-code --print "summarize this repo"
360
+
361
+ # Pipe a prompt in — the CLI auto-enters headless mode
362
+ printf 'add a .gitignore\n' | savant-code
363
+ ```
364
+
365
+ Exit codes: `0` success, `1` error or timeout, `2` usage error.
366
+ `SAVANT_CODE_RUN_TIMEOUT_MS` (default 10 minutes) bounds hung runs.
367
+
347
368
  ## Troubleshooting
348
369
 
349
370
  ### 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.21",
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>",