@swarmclawai/swarmclaw 1.5.34 → 1.5.36
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 +27 -0
- package/bin/update-cmd.js +26 -0
- package/package.json +13 -1
- package/scripts/easy-update.mjs +23 -0
- package/src/app/api/setup/doctor/route.ts +31 -1
- package/src/app/api/version/update/route.ts +17 -0
- package/src/components/auth/access-key-gate.tsx +0 -24
- package/src/components/layout/sidebar-rail.tsx +0 -47
- package/src/lib/server/credentials/credential-service.ts +9 -1
- package/src/lib/server/openclaw/gateway.ts +8 -1
- package/src/lib/server/storage.ts +15 -0
package/README.md
CHANGED
|
@@ -69,6 +69,19 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
|
|
|
69
69
|
|
|
70
70
|
## Quick Start
|
|
71
71
|
|
|
72
|
+
### Desktop app (recommended for non-technical users)
|
|
73
|
+
|
|
74
|
+
Download the one-click installer from [swarmclaw.ai/downloads](https://swarmclaw.ai/downloads).
|
|
75
|
+
Available for macOS (Apple Silicon & Intel), Windows, and Linux (AppImage + .deb).
|
|
76
|
+
|
|
77
|
+
Current builds are unsigned, so on first launch:
|
|
78
|
+
- **macOS:** right-click the app in Finder → **Open** → **Open** to bypass Gatekeeper.
|
|
79
|
+
- **Windows:** if SmartScreen appears, click **More info** → **Run anyway**.
|
|
80
|
+
- **Linux (AppImage):** `chmod +x` the downloaded file, then run it.
|
|
81
|
+
|
|
82
|
+
Data lives in your OS app-data directory (`~/Library/Application Support/SwarmClaw`,
|
|
83
|
+
`%APPDATA%\SwarmClaw`, or `~/.config/SwarmClaw`), separate from any CLI or Docker install.
|
|
84
|
+
|
|
72
85
|
### Global install
|
|
73
86
|
|
|
74
87
|
```bash
|
|
@@ -375,6 +388,20 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
375
388
|
|
|
376
389
|
## Releases
|
|
377
390
|
|
|
391
|
+
### v1.5.36 Highlights
|
|
392
|
+
|
|
393
|
+
- **Desktop app (Electron)**: SwarmClaw now ships as a native desktop app for macOS (Apple Silicon + Intel), Windows, and Linux (AppImage + .deb). Download from [swarmclaw.ai/downloads](https://swarmclaw.ai/downloads). The app wraps the existing standalone server inside an Electron shell, stores data in the OS app-data directory, and auto-updates via GitHub Releases (notify-only on unsigned macOS builds).
|
|
394
|
+
- **Desktop release CI**: new `desktop-release.yml` workflow builds and publishes installers for all three platforms to GitHub Releases on every version tag.
|
|
395
|
+
- **UI cleanup**: removed sibling-product navigation links from the in-app sidebar rail and login gate so the open-source app focuses on SwarmClaw itself. Those links remain in the project README and on swarmclaw.ai.
|
|
396
|
+
|
|
397
|
+
### v1.5.35 Highlights
|
|
398
|
+
|
|
399
|
+
- **Update safety: prevent DB corruption on Linux**: `npm run update:easy`, `swarmclaw update`, and the in-app update endpoint now stop the running server (or checkpoint the SQLite WAL) before rebuilding native modules, preventing the WAL journal corruption that forced some Linux users back to the setup wizard.
|
|
400
|
+
- **SQLite graceful shutdown**: the server now checkpoints and closes the database on SIGTERM/SIGINT, eliminating stale WAL state after any clean stop.
|
|
401
|
+
- **Doctor: detect dangling gateway credentials**: the setup doctor now flags gateway profiles that reference deleted or missing credentials, explaining the "gateway token missing" connection errors.
|
|
402
|
+
- **Gateway credential resolution logging**: when a gateway credential can't be resolved, the server now logs a clear warning identifying the missing credential ID.
|
|
403
|
+
- **Credential decryption error logging**: when a stored credential can't be decrypted (e.g. after `CREDENTIAL_SECRET` changes), the server now logs the credential ID and provider so users know which key to re-add.
|
|
404
|
+
|
|
378
405
|
### v1.5.34 Highlights
|
|
379
406
|
|
|
380
407
|
- **Ollama Cloud auth fix**: SwarmClaw now normalizes `api.ollama.com` and `www.ollama.com` to `ollama.com` before making authenticated requests, avoiding the redirect that was dropping authorization headers and causing false provider-health/runtime failures.
|
package/bin/update-cmd.js
CHANGED
|
@@ -98,6 +98,29 @@ function runRegistrySelfUpdate(
|
|
|
98
98
|
return 0
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function stopRunningServer(logger = { log, logError }) {
|
|
102
|
+
// Stop the server gracefully before updating to prevent SQLite WAL corruption.
|
|
103
|
+
try {
|
|
104
|
+
const serverCmd = path.join(PKG_ROOT, 'bin', 'swarmclaw.js')
|
|
105
|
+
const status = execFileSync(process.execPath, [serverCmd, 'status'], {
|
|
106
|
+
encoding: 'utf-8',
|
|
107
|
+
cwd: PKG_ROOT,
|
|
108
|
+
timeout: 10_000,
|
|
109
|
+
}).trim()
|
|
110
|
+
if (status.toLowerCase().includes('running')) {
|
|
111
|
+
logger.log('Stopping running server before update...')
|
|
112
|
+
execFileSync(process.execPath, [serverCmd, 'stop'], {
|
|
113
|
+
encoding: 'utf-8',
|
|
114
|
+
cwd: PKG_ROOT,
|
|
115
|
+
timeout: 15_000,
|
|
116
|
+
})
|
|
117
|
+
logger.log('Server stopped.')
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Server may not be running or status command unavailable — continue.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
function main(args = process.argv.slice(3)) {
|
|
102
125
|
if (args.includes('-h') || args.includes('--help')) {
|
|
103
126
|
console.log(`
|
|
@@ -112,9 +135,12 @@ If running from a registry install, update the global package with its owning pa
|
|
|
112
135
|
try {
|
|
113
136
|
run('git rev-parse --git-dir')
|
|
114
137
|
} catch {
|
|
138
|
+
stopRunningServer()
|
|
115
139
|
process.exit(runRegistrySelfUpdate())
|
|
116
140
|
}
|
|
117
141
|
|
|
142
|
+
stopRunningServer()
|
|
143
|
+
|
|
118
144
|
const beforeRef = run('git rev-parse HEAD')
|
|
119
145
|
const beforeSha = run('git rev-parse --short HEAD')
|
|
120
146
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.36",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
|
+
"main": "electron-dist/main.js",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"publishConfig": {
|
|
7
8
|
"access": "public",
|
|
@@ -78,6 +79,13 @@
|
|
|
78
79
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
79
80
|
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
|
|
80
81
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
82
|
+
"electron:compile": "tsc -p electron/tsconfig.json",
|
|
83
|
+
"electron:dev": "npm run electron:compile && electron electron-dist/main.js",
|
|
84
|
+
"electron:build": "node ./scripts/build-electron.mjs",
|
|
85
|
+
"electron:build:mac": "node ./scripts/build-electron.mjs --mac",
|
|
86
|
+
"electron:build:win": "node ./scripts/build-electron.mjs --win",
|
|
87
|
+
"electron:build:linux": "node ./scripts/build-electron.mjs --linux",
|
|
88
|
+
"electron:build:publish": "node ./scripts/build-electron.mjs --publish",
|
|
81
89
|
"prepack": "npm run build:ci",
|
|
82
90
|
"postinstall": "node ./scripts/postinstall.mjs"
|
|
83
91
|
},
|
|
@@ -114,6 +122,7 @@
|
|
|
114
122
|
"class-variance-authority": "^0.7.1",
|
|
115
123
|
"clsx": "^2.1.1",
|
|
116
124
|
"commander": "^13.1.0",
|
|
125
|
+
"electron-updater": "^6.3.9",
|
|
117
126
|
"cron-parser": "^5.5.0",
|
|
118
127
|
"cronstrue": "^3.12.0",
|
|
119
128
|
"dagre": "^0.8.5",
|
|
@@ -156,7 +165,10 @@
|
|
|
156
165
|
"zustand": "^5.0.11"
|
|
157
166
|
},
|
|
158
167
|
"devDependencies": {
|
|
168
|
+
"@electron/rebuild": "^3.7.2",
|
|
159
169
|
"@types/dagre": "^0.7.54",
|
|
170
|
+
"electron": "^33.3.0",
|
|
171
|
+
"electron-builder": "^25.1.8",
|
|
160
172
|
"eslint": "^9",
|
|
161
173
|
"eslint-config-next": "16.1.7"
|
|
162
174
|
},
|
package/scripts/easy-update.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
3
5
|
import { spawnSync } from 'node:child_process'
|
|
4
6
|
|
|
5
7
|
const args = new Set(process.argv.slice(2))
|
|
@@ -68,12 +70,33 @@ function getLatestStableTag() {
|
|
|
68
70
|
return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
function stopRunningServer() {
|
|
74
|
+
// Try to stop the SwarmClaw server gracefully before updating.
|
|
75
|
+
// An unclean shutdown while npm rebuild replaces native modules
|
|
76
|
+
// can corrupt the SQLite WAL journal on Linux.
|
|
77
|
+
const cliPath = path.join(cwd, 'bin', 'swarmclaw.js')
|
|
78
|
+
if (!fs.existsSync(cliPath)) return
|
|
79
|
+
|
|
80
|
+
const status = run('node', [cliPath, 'status'])
|
|
81
|
+
if (!status.ok || !status.out.toLowerCase().includes('running')) return
|
|
82
|
+
|
|
83
|
+
log('Stopping running SwarmClaw server before update...')
|
|
84
|
+
const stop = run('node', [cliPath, 'stop'])
|
|
85
|
+
if (stop.ok) {
|
|
86
|
+
log('Server stopped.')
|
|
87
|
+
} else {
|
|
88
|
+
log('Could not stop the server automatically. Please stop it manually before updating.')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function main() {
|
|
72
93
|
const gitCheck = run('git', ['rev-parse', '--is-inside-work-tree'])
|
|
73
94
|
if (!gitCheck.ok) {
|
|
74
95
|
fail('This folder is not a git repository. Automatic updates require git.')
|
|
75
96
|
}
|
|
76
97
|
|
|
98
|
+
stopRunningServer()
|
|
99
|
+
|
|
77
100
|
const dirty = run('git', ['status', '--porcelain'])
|
|
78
101
|
const isDirty = !!dirty.out
|
|
79
102
|
if (isDirty && !allowDirty) {
|
|
@@ -3,7 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
4
|
import { NextResponse } from 'next/server'
|
|
5
5
|
import { DATA_DIR } from '@/lib/server/data-dir'
|
|
6
|
-
import { loadAgents, loadCredentials, loadSettings } from '@/lib/server/storage'
|
|
6
|
+
import { loadAgents, loadCredentials, loadSettings, loadCollection } from '@/lib/server/storage'
|
|
7
7
|
import { dedup, errorMessage } from '@/lib/shared-utils'
|
|
8
8
|
import { detectDocker } from '@/lib/server/sandbox/docker-detect'
|
|
9
9
|
|
|
@@ -160,6 +160,7 @@ export async function GET(req: Request) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const credentials = Object.values(loadCredentials() || {})
|
|
163
|
+
const credentialIds = new Set(credentials.map((c) => (c as Record<string, unknown>).id as string).filter(Boolean))
|
|
163
164
|
if (credentials.length > 0) {
|
|
164
165
|
pushCheck(checks, 'credentials', 'Credentials', 'pass', `${credentials.length} credential(s) saved.`)
|
|
165
166
|
} else {
|
|
@@ -167,6 +168,35 @@ export async function GET(req: Request) {
|
|
|
167
168
|
actions.push('If using cloud providers, add an API key in the setup wizard or Settings → Providers.')
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
// Check for undecryptable credentials (CREDENTIAL_SECRET may have changed)
|
|
172
|
+
if (credentials.length > 0) {
|
|
173
|
+
try {
|
|
174
|
+
const { resolveCredentialSecret } = await import('@/lib/server/credentials/credential-service')
|
|
175
|
+
let undecryptable = 0
|
|
176
|
+
for (const cred of credentials) {
|
|
177
|
+
const c = cred as Record<string, unknown>
|
|
178
|
+
if (c.encryptedKey && !resolveCredentialSecret(c.id as string)) undecryptable++
|
|
179
|
+
}
|
|
180
|
+
if (undecryptable > 0) {
|
|
181
|
+
pushCheck(checks, 'credential-decrypt', 'Credential decryption', 'fail',
|
|
182
|
+
`${undecryptable} of ${credentials.length} credential(s) cannot be decrypted. CREDENTIAL_SECRET may have changed since these keys were stored.`, true)
|
|
183
|
+
actions.push('Your CREDENTIAL_SECRET changed (possibly from a container restart). Re-add your API keys in Settings → Providers.')
|
|
184
|
+
}
|
|
185
|
+
} catch { /* best-effort */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for dangling credential references in gateway profiles
|
|
189
|
+
try {
|
|
190
|
+
const gateways = Object.values(loadCollection('gateway_profiles') || {}) as Array<Record<string, unknown>>
|
|
191
|
+
const dangling = gateways.filter((gw) => gw.credentialId && !credentialIds.has(gw.credentialId as string))
|
|
192
|
+
if (dangling.length > 0) {
|
|
193
|
+
const names = dangling.map((gw) => gw.name || gw.id).join(', ')
|
|
194
|
+
pushCheck(checks, 'gateway-credentials', 'Gateway credential references', 'warn',
|
|
195
|
+
`${dangling.length} gateway profile(s) reference missing credentials: ${names}. This causes "gateway token missing" errors.`)
|
|
196
|
+
actions.push('Re-add the missing API key in Settings → Gateways, or update the gateway profile to use an existing credential.')
|
|
197
|
+
}
|
|
198
|
+
} catch { /* best-effort */ }
|
|
199
|
+
|
|
170
200
|
const optionalBinaries: Array<{ id: string; label: string; command: string }> = [
|
|
171
201
|
{ id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
|
|
172
202
|
{ id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { execSync } from 'child_process'
|
|
3
|
+
import { getDb } from '@/lib/server/storage'
|
|
3
4
|
|
|
4
5
|
const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/
|
|
5
6
|
|
|
@@ -7,6 +8,19 @@ function run(cmd: string): string {
|
|
|
7
8
|
return execSync(cmd, { encoding: 'utf-8', cwd: process.cwd(), timeout: 60_000 }).trim()
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Checkpoint the SQLite WAL before operations that replace native modules
|
|
13
|
+
* (npm install rebuilds better-sqlite3). Without this, an unclean WAL state
|
|
14
|
+
* combined with a replaced native binary can corrupt the database on Linux.
|
|
15
|
+
*/
|
|
16
|
+
function checkpointDatabase(): void {
|
|
17
|
+
try {
|
|
18
|
+
getDb().pragma('wal_checkpoint(TRUNCATE)')
|
|
19
|
+
} catch {
|
|
20
|
+
// Best-effort — the database may already be in a good state.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
function getLatestStableTag(): string | null {
|
|
11
25
|
const tags = run(`git tag --list 'v*' --sort=-v:refname`)
|
|
12
26
|
.split('\n')
|
|
@@ -67,6 +81,9 @@ export async function POST() {
|
|
|
67
81
|
try {
|
|
68
82
|
const diff = run(`git diff --name-only ${beforeSha}..HEAD`)
|
|
69
83
|
if (diff.includes('package-lock.json') || diff.includes('package.json')) {
|
|
84
|
+
// Checkpoint WAL before npm install — the postinstall hook rebuilds
|
|
85
|
+
// better-sqlite3's native module, which can corrupt an open WAL journal.
|
|
86
|
+
checkpointDatabase()
|
|
70
87
|
run('npm install --omit=dev')
|
|
71
88
|
installedDeps = true
|
|
72
89
|
}
|
|
@@ -9,11 +9,6 @@ interface AccessKeyGateProps {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const AUTH_CHECK_TIMEOUT_MS = 8_000
|
|
12
|
-
const NETWORK_LINKS = [
|
|
13
|
-
{ href: 'https://www.swarmdock.ai', label: 'SwarmDock' },
|
|
14
|
-
{ href: 'https://swarmrecall.ai', label: 'SwarmRecall' },
|
|
15
|
-
{ href: 'https://swarmrelay.ai', label: 'SwarmRelay' },
|
|
16
|
-
]
|
|
17
12
|
|
|
18
13
|
function isExpectedAuthCheckError(err: unknown): boolean {
|
|
19
14
|
return isAbortError(err) || isTimeoutError(err)
|
|
@@ -427,25 +422,6 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
|
|
|
427
422
|
</>
|
|
428
423
|
)}
|
|
429
424
|
|
|
430
|
-
<div className="mt-10 border-t border-white/[0.06] pt-5" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
|
|
431
|
-
<p className="text-[10px] font-700 uppercase tracking-[0.18em] text-text-3/55">
|
|
432
|
-
Network
|
|
433
|
-
</p>
|
|
434
|
-
<div className="mt-3 flex flex-wrap items-center justify-center gap-2.5">
|
|
435
|
-
{NETWORK_LINKS.map((link) => (
|
|
436
|
-
<a
|
|
437
|
-
key={link.href}
|
|
438
|
-
href={link.href}
|
|
439
|
-
target="_blank"
|
|
440
|
-
rel="noopener noreferrer"
|
|
441
|
-
className="rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1.5 text-[12px] text-text-3
|
|
442
|
-
no-underline transition-all duration-200 hover:border-white/[0.14] hover:bg-white/[0.06] hover:text-text"
|
|
443
|
-
>
|
|
444
|
-
{link.label}
|
|
445
|
-
</a>
|
|
446
|
-
))}
|
|
447
|
-
</div>
|
|
448
|
-
</div>
|
|
449
425
|
</div>
|
|
450
426
|
</div>
|
|
451
427
|
)
|
|
@@ -17,11 +17,6 @@ import type { AppView } from '@/types'
|
|
|
17
17
|
const RAIL_EXPANDED_KEY = 'sc_rail_expanded'
|
|
18
18
|
const GITHUB_REPO_URL = 'https://github.com/swarmclawai/swarmclaw'
|
|
19
19
|
const DISCORD_URL = 'https://discord.gg/sbEavS8cPV'
|
|
20
|
-
const NETWORK_LINKS = [
|
|
21
|
-
{ href: 'https://www.swarmdock.ai', label: 'SwarmDock', abbr: 'DO' },
|
|
22
|
-
{ href: 'https://swarmrecall.ai', label: 'SwarmRecall', abbr: 'RE' },
|
|
23
|
-
{ href: 'https://swarmrelay.ai', label: 'SwarmRelay', abbr: 'RL' },
|
|
24
|
-
]
|
|
25
20
|
|
|
26
21
|
export function SidebarRail({
|
|
27
22
|
onSwitchUser,
|
|
@@ -394,48 +389,6 @@ export function SidebarRail({
|
|
|
394
389
|
|
|
395
390
|
{/* Bottom: Docs + Daemon + Settings + User */}
|
|
396
391
|
<div className={`flex flex-col gap-1 ${railExpanded ? 'px-3' : 'items-center'}`}>
|
|
397
|
-
{railExpanded ? (
|
|
398
|
-
<div className="mb-1">
|
|
399
|
-
<div className="px-3 pb-1 text-[10px] font-700 uppercase tracking-[0.12em] text-text-3/45">Network</div>
|
|
400
|
-
<div className="flex flex-col gap-1">
|
|
401
|
-
{NETWORK_LINKS.map((link) => (
|
|
402
|
-
<a
|
|
403
|
-
key={link.href}
|
|
404
|
-
href={link.href}
|
|
405
|
-
target="_blank"
|
|
406
|
-
rel="noopener noreferrer"
|
|
407
|
-
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-[10px] text-[13px] font-500 cursor-pointer transition-all
|
|
408
|
-
bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04] no-underline"
|
|
409
|
-
style={{ fontFamily: 'inherit' }}
|
|
410
|
-
>
|
|
411
|
-
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-[6px] border border-white/[0.08] bg-white/[0.03] text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/80">
|
|
412
|
-
{link.abbr}
|
|
413
|
-
</span>
|
|
414
|
-
{link.label}
|
|
415
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="ml-auto opacity-40">
|
|
416
|
-
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
|
|
417
|
-
</svg>
|
|
418
|
-
</a>
|
|
419
|
-
))}
|
|
420
|
-
</div>
|
|
421
|
-
</div>
|
|
422
|
-
) : (
|
|
423
|
-
<>
|
|
424
|
-
<div className="my-1 h-px w-6 bg-white/[0.06]" />
|
|
425
|
-
{NETWORK_LINKS.map((link) => (
|
|
426
|
-
<RailTooltip key={link.href} label={link.label} description="Open product site in a new tab">
|
|
427
|
-
<a
|
|
428
|
-
href={link.href}
|
|
429
|
-
target="_blank"
|
|
430
|
-
rel="noopener noreferrer"
|
|
431
|
-
className="rail-btn text-[10px] font-700 uppercase tracking-[0.08em] no-underline"
|
|
432
|
-
>
|
|
433
|
-
{link.abbr}
|
|
434
|
-
</a>
|
|
435
|
-
</RailTooltip>
|
|
436
|
-
))}
|
|
437
|
-
</>
|
|
438
|
-
)}
|
|
439
392
|
{railExpanded ? (
|
|
440
393
|
<a
|
|
441
394
|
href="https://swarmclaw.ai/docs"
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
loadCredentials,
|
|
10
10
|
saveCredential,
|
|
11
11
|
} from '@/lib/server/credentials/credential-repository'
|
|
12
|
+
import { log } from '@/lib/server/logger'
|
|
13
|
+
|
|
14
|
+
const TAG = 'credential-service'
|
|
12
15
|
|
|
13
16
|
export type CredentialSummary = Pick<Credential, 'id' | 'provider' | 'name' | 'createdAt'>
|
|
14
17
|
|
|
@@ -55,7 +58,12 @@ export function resolveCredentialSecret(credentialId: string | null | undefined)
|
|
|
55
58
|
if (!credential?.encryptedKey) return null
|
|
56
59
|
try {
|
|
57
60
|
return decryptKey(credential.encryptedKey)
|
|
58
|
-
} catch {
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.warn(TAG, `Failed to decrypt credential "${id}" — CREDENTIAL_SECRET may have changed since this key was stored. Re-add the API key to fix.`, {
|
|
63
|
+
credentialId: id,
|
|
64
|
+
provider: credential.provider,
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
})
|
|
59
67
|
return null
|
|
60
68
|
}
|
|
61
69
|
}
|
|
@@ -81,7 +81,14 @@ function normalizeWsUrl(raw: string): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function resolveTokenForCredential(credentialId?: string | null): string | undefined {
|
|
84
|
-
|
|
84
|
+
if (!credentialId) return undefined
|
|
85
|
+
const secret = resolveCredentialSecret(credentialId)
|
|
86
|
+
if (!secret) {
|
|
87
|
+
log.warn(TAG, `Credential "${credentialId}" is referenced but could not be resolved — gateway connection will lack a token`, {
|
|
88
|
+
credentialId,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
return secret || undefined
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
export function resolveGatewayConfig(target?: {
|
|
@@ -94,6 +94,21 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
94
94
|
}
|
|
95
95
|
db.pragma('foreign_keys = ON')
|
|
96
96
|
|
|
97
|
+
// Graceful shutdown: checkpoint WAL and close the database to prevent
|
|
98
|
+
// corruption when the process is killed (e.g. during npm run update:easy).
|
|
99
|
+
if (!IS_BUILD_BOOTSTRAP) {
|
|
100
|
+
const shutdownDb = () => {
|
|
101
|
+
try {
|
|
102
|
+
db.pragma('wal_checkpoint(TRUNCATE)')
|
|
103
|
+
db.close()
|
|
104
|
+
} catch {
|
|
105
|
+
// Best-effort — process is exiting.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
process.on('SIGTERM', shutdownDb)
|
|
109
|
+
process.on('SIGINT', shutdownDb)
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
/** Run a function inside an immediate SQLite transaction for atomicity. */
|
|
98
113
|
export function withTransaction<T>(fn: () => T): T {
|
|
99
114
|
const wrapped = db.transaction(fn)
|