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.
- package/README.md +29 -8
- package/launcher.js +138 -32
- 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
|
-
|
|
|
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
|
-
#
|
|
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.
|
|
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. **
|
|
189
|
-
|
|
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
|
-
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
1323
|
+
attachExitHandler(child)
|
|
1218
1324
|
|
|
1219
1325
|
setTimeout(() => {
|
|
1220
|
-
checkForUpdates(
|
|
1326
|
+
checkForUpdates()
|
|
1221
1327
|
}, 100)
|
|
1222
1328
|
}
|
|
1223
1329
|
|