Skip to content
This repository was archived by the owner on May 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ All notable changes to `@krawlerhq/agent` land here. Format follows [Keep a Chan

Nothing queued yet.

## [0.12.12] - 2026-04-21
## [0.12.13] - 2026-04-22

### Fixed

- **`saveConfig()` no longer wipes shared-keys.json with empty defaults.** When a caller passed a full `Config` object through `saveConfig` (notably the `loadConfig()` fresh-install branch, which parses schema defaults like `anthropicApiKey: ''`), every empty-string shared-key field was routed into `saveSharedKeys`, overwriting any keys the user had already pasted. Now empty-string shared-key values are filtered out of the split; callers that legitimately need to clear a key should call `saveSharedKeys` directly. Users upgrading from 0.12.12 or earlier whose keys vanished after a `config.json` reset will stop losing them.

### Changed

- **OpenRouter provider option now reads just "OpenRouter".** The old `OpenRouter (100s of models)` label was both misleading (the dropdown is a curated top 10 as of 0.12.12) and noisy in the provider `<select>`. Clean label matches the others (`Anthropic`, `OpenAI`, `Google`, `Ollama (local)`).
- **Populated API-key fields show a "Saved" chip + green accent.** Keys that already exist in `shared-keys.json` now surface a clear visual: a green "✓ saved" pill next to the field label, a tinted green input border, and a subtly mint-tinted background. The prior hint text (`currently: sk-ant-••••••abcd · leave blank to keep`) stays below the input with a small copy tweak ("paste a new one to replace"). Empty slots still render in the standard neutral style, so the eye can scan the form for which providers still need keys without reading every hint line.

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@krawlerhq/agent",
"version": "0.12.12",
"version": "0.12.13",
"description": "Your personal AI agent, living locally, with a public identity on Krawler. Chat with it in the terminal; it posts, follows, endorses, remembers, and learns. Bring your own model (Anthropic, OpenAI, Google, OpenRouter, Ollama).",
"keywords": [
"krawler",
Expand Down
10 changes: 10 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,16 @@ export function saveConfig(c: Partial<Config>): Config {
const profilePartial: Partial<Config> = {};
for (const [k, v] of Object.entries(c)) {
if ((SHARED_KEY_FIELDS as readonly string[]).includes(k)) {
// Empty-string shared-key fields are "no info", not "clear this
// key". If we routed them through, saveSharedKeys would overwrite
// existing real values with empty strings every time a caller
// passed a full Config object through saveConfig() — including
// the loadConfig() fresh-install path, which parses schema
// defaults (anthropicApiKey: '', etc.) and echoes them here.
// That silently wiped any shared-keys.json the user had already
// populated out-of-band. Callers that legitimately need to CLEAR
// a key should do so via saveSharedKeys directly.
if (typeof v === 'string' && v === '') continue;
(sharedPartial as Record<string, unknown>)[k] = v;
} else {
(profilePartial as Record<string, unknown>)[k] = v;
Expand Down
16 changes: 11 additions & 5 deletions src/key-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,13 @@ function renderPage(existing: SharedKeys, activeProvider: Provider, activeModel:
{ id: 'openrouterApiKey', label: 'OpenRouter', placeholder: 'sk-or-v1-...', value: existing.openrouterApiKey, existing: mask(existing.openrouterApiKey) },
];
const fieldHtml = fields.map((f) => `
<div class="row">
<label for="${f.id}">${f.label} key</label>
<div class="row${f.existing ? ' is-set' : ''}">
<label for="${f.id}">
<span>${f.label} key</span>
${f.existing ? '<span class="badge-set">\u2713 saved</span>' : ''}
</label>
<input type="password" id="${f.id}" name="${f.id}" autocomplete="off" placeholder="${f.existing || f.placeholder}" />
${f.existing ? `<div class="hint">currently: <code>${f.existing}</code> \u00b7 leave blank to keep</div>` : ''}
${f.existing ? `<div class="hint">currently: <code>${f.existing}</code> \u00b7 leave blank to keep, paste a new one to replace</div>` : ''}
</div>
`).join('');
const providerOptions = PROVIDERS
Expand Down Expand Up @@ -107,8 +110,11 @@ function renderPage(existing: SharedKeys, activeProvider: Provider, activeModel:
.sub { color: var(--text-2); font-size: 0.95rem; margin: 0 0 32px; line-height: 1.55; }
.row { margin-bottom: 22px; }
.row:last-child { margin-bottom: 0; }
label { display: block; font-weight: 600; font-size: 0.82rem; margin-bottom: 6px; color: var(--text-2); }
label { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 0.82rem; margin-bottom: 6px; color: var(--text-2); }
.badge-set { display: inline-flex; align-items: center; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: #065f46; background: #d1fae5; border: 1px solid #a7f3d0; padding: 1px 8px; border-radius: 9999px; line-height: 1.4; font-family: -apple-system, system-ui, "Segoe UI", Roboto, sans-serif; }
input[type=password], input[type=text], input[type=url], select { width: 100%; padding: 11px 14px; border: 1px solid var(--border); border-radius: 10px; font: inherit; font-size: 0.95rem; background: var(--surface); font-family: var(--mono); color: var(--text); }
.row.is-set input[type=password], .row.is-set input[type=text], .row.is-set input[type=url] { border-color: #6ee7b7; background: #f0fdf4; }
.row.is-set input:focus { border-color: var(--brand); background: var(--surface); }
select { cursor: pointer; appearance: none; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M3 4.5L6 7.5 9 4.5' stroke='%236b7280' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px; }
input:focus, select:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(37,99,235,0.18); }
.section { padding-top: 28px; margin-top: 28px; border-top: 1px solid var(--border); }
Expand Down Expand Up @@ -372,7 +378,7 @@ function providerLabel(p: Provider): string {
case 'anthropic': return 'Anthropic';
case 'openai': return 'OpenAI';
case 'google': return 'Google';
case 'openrouter': return 'OpenRouter (100s of models)';
case 'openrouter': return 'OpenRouter';
case 'ollama': return 'Ollama (local)';
}
}
Expand Down