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.
- package/README.md +37 -13
- package/launcher.js +138 -32
- 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
|
-
|
|
49
|
-
|
|
|
50
|
-
| TokenRouter | `/provider tokenrouter` | `TOKENROUTER_API_KEY` | Multi-provider gateway |
|
|
51
|
-
| NVIDIA NIM | `/provider nvidia` | `NVIDIA_API_KEY` | NVIDIA-hosted inference |
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
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
|
-
#
|
|
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.
|
|
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. **
|
|
189
|
-
|
|
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
|
-
-
|
|
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.
|
|
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(
|
|
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
|
|