Skip to content

feat(cli,db): 2.C — provider/key commands (OS keychain via @napi-rs/keyring)#45

Merged
cemililik merged 10 commits into
mainfrom
development
Jun 23, 2026
Merged

feat(cli,db): 2.C — provider/key commands (OS keychain via @napi-rs/keyring)#45
cemililik merged 10 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What & why

Phase-2 workstream 2.Crelavium provider commands and OS-keychain key storage. The CLI can now
register providers and manage API keys in the platform keychain, and relavium run / provider test
resolve keys from it. No engine/core change — the key source plugs into the existing ProviderResolver
seam (ADR-0038); packages/core is untouched.

What's in it

  • New dep @napi-rs/keyring (a maintained N-API credential lib — not the archived keytar,
    pre-decided by ADR-0019, so no new ADR). Confined
    behind a KeychainStore interface: the pure seam is secrets/keychain.ts; the native accessor in
    secrets/os-keychain.ts is the only module that loads it, so commands/resolver unit-test with an
    in-memory fake and never touch the real keychain.
  • @relavium/db createProviderStore — CRUD over the existing llm_providers catalog. The key
    value is never persisted
    (the schema has no column for it) — only api_key_keychain_ref, the
    {providerId}:default account, per ADR-0006.
  • relavium providerlist / add / set-key / remove-key / test. set-key reads the key
    from stdin (never argv — no ps/shell-history/CI-log leak); display is hint-only (last 4);
    provider test does a 1-token generate through @relavium/llm.
  • keyFor resolution chain: OS keychain → RELAVIUM_<PROVIDER>_API_KEY env var → error. A
    write (set-key) surfaces an unavailable keychain (no silent plaintext fallback); the read resolver
    falls through to the env var (the documented headless path).
  • Shared db-open extracted to db/open.ts (history + provider commands reuse it).

Decision (maintainer-approved)

The secrets.enc encrypted-file fallback is deferred past v1.0 — it requires a proper Argon2id KDF,
not a hand-rolled scheme — per the canonical keychain-and-secrets.md. v1.0 is keychain + env var.
§2.C, config-spec.md, and keychain-and-secrets.md are reconciled to match, and the provider
surface is documented in commands.md. No new ADR.

Also in this PR

Tests

  • @relavium/db: provider-store CRUD, idempotent upsert, the never-store-the-key guarantee (no default_headers double-encode).
  • apps/cli: the provider commands (in-memory keychain + :memory: db + stub provider), the keyFor
    keychain→env→error chain, keyHint, the stdin TTY guard, key-redaction in a failing test, and
    never-the-key assertions throughout.

Review trail

A relavium-reviewer pass (7 findings, fixed in 6caf969) + three independent external reviews (two full
PASS on the security posture; one /security-review blocked by a secrets/ path-deny — the other two read
those files and verified the native bridge + stdin reader clean). Their findings — default_headers
double-encode, --base-url validation, key redaction, set-key base-URL preservation, the stdin TTY guard
— are fixed in 8ae794c / 6caf969.

Verification

pnpm turbo run lint typecheck test build green; format clean; engine-deps within allowlists; the
@relavium/llm seam holds (only Relavium types cross); @napi-rs/keyring confined to os-keychain.ts;
Leakwatch 0; packages/core untouched.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a confirmed relavium provider command (list, add, set-key, remove-key, test) for storing provider API credentials in the OS keychain.
    • set-key is stdin-only, and add --base-url is validated as HTTPS-only.
    • Durable local run history is enabled with 0600-restricted history DB files.
  • Bug Fixes
    • Prevented key leakage by masking/redacting API keys in outputs and error messages.
  • Documentation
    • Updated CLI reference, roadmap, and secret-handling docs for key resolution precedence and provider workflow.
  • Tests
    • Added comprehensive CLI provider/key-resolution tests, including failure and masking behaviors.

cemililik and others added 4 commits June 23, 2026 10:29
…xt pickup 2.C

PR #44 merged: durable local run history via @relavium/db (the SQLite RunStore writer +
read API + ADR-0050 at-rest posture) is live.

- phase-2-cli.md: §2.H header ✅ Done (PR #44); status line records 2.H Done (ADR-0050); the
  "Remaining build order" queue drops the completed 2.H row, renumbers (2.C is now #1), and
  the gate-closing backbone is 2.C → 2.E → 2.G → 2.I → 2.L (2.K + 2.H done).
- current.md: 2.H ✅ Done; "Next pickup: 2.C" (provider/keys).
- CLAUDE.md + README.md: Phase-2 status sentence records durable run history landed.

Next pickup is 2.C (provider/keys) — independent, unblocks 2.R + 2.M + live runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eyring)

Add `relavium provider` (list / add / set-key / remove-key / test) and wire the OS keychain
as the primary key source behind the existing `ProviderResolver` seam. No engine/core change.

- New dep `@napi-rs/keyring` (a maintained N-API credential lib — NOT the archived keytar,
  ADR-0019; the decision pre-exists, no new ADR). Confined behind a `KeychainStore` interface:
  the pure seam lives in secrets/keychain.ts; the native accessor in secrets/os-keychain.ts is
  the only module that loads it, so commands/resolver unit-test with an in-memory fake.
- @relavium/db: createProviderStore — CRUD over the existing `llm_providers` catalog. The key
  VALUE is never stored (the schema has no column for it); only `api_key_keychain_ref`, the
  `{providerId}:default` account, plus the non-secret row (ADR-0006).
- providers.ts: keyFor is now OS keychain → `RELAVIUM_<PROVIDER>_API_KEY` env var → error; an
  unavailable backend falls through to env (no silent plaintext fallback). keyHint (last 4)
  for display; the full key never reaches stdout/logs/--json. `set-key` reads the key from
  STDIN, never argv. `provider test` does a 1-token generate through @relavium/llm.
- Shared db-open extracted to db/open.ts (history + provider commands reuse it).
- Decision (maintainer-approved): the `secrets.enc` encrypted-file fallback is DEFERRED past
  v1.0 (it needs a proper Argon2id KDF) per the canonical keychain-and-secrets.md — v1.0 is
  keychain + env var. Reconciled §2.C + config-spec.md to match; documented the `provider`
  surface in commands.md.

Tests: db 4 provider-store + cli 13 (commands with in-memory keychain/`:memory:` db + stub
provider, the keyFor chain, keyHint, never-the-key assertions). Full gate green; engine purity
+ @relavium/llm seam intact (only Relavium seam types imported); Leakwatch 0.

Refs: ADR-0019, ADR-0006, ADR-0038, phase-2-cli.md 2.C

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… keychain docs)

Address the relavium-reviewer findings on 2.C (0 blockers):

- [High] provider set-key no longer clobbers a custom base URL: it registers the row only
  when absent, so `provider add <p> --base-url <url>` survives a later `set-key`. + regression test.
- [Med] readSecretFromStdin guards on `process.stdin.isTTY` alone (was `&& io.stdoutIsTty`),
  so `set-key … > out.txt` (stdin still a TTY) still errors instead of reading + echoing a typed
  key. Dropped the now-unused `io` param. + a TTY-guard test.
- [Med] Reconcile the KeychainUnavailableError contract: the JSDoc now states the WRITE path
  (set-key) surfaces it while the run-time resolver (keyFor) falls through to the env var (the
  CLI's documented no-keychain path — an env var is not an on-disk plaintext store). Recorded the
  same in keychain-and-secrets.md §Operational notes.
- [Low] The test-failure test now asserts the resolved key never appears in the error message.
- [Low] provider action handlers use explicit `await dispatch(...)` (consistent with run.action).
- [Nit] readSecretFromStdin uses a `Buffer.isBuffer` guard instead of an unsafe `as Buffer`.

cli 140 tests; full gate green; Leakwatch 0; seam + engine purity intact.

Refs: ADR-0019, ADR-0006, phase-2-cli.md 2.C

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…base-url validation, key redaction

Address the external review findings (0 blockers; 1 Medium + 3 Low + the SSRF forward-obligation):

- [Med] provider-store upsert UPDATE no longer double-encodes `default_headers`: the existing column
  is already a JSON string, so it's kept verbatim when the caller passes none (re-stringifying it
  produced `"{}"`, then `"\"{}\""`, accumulating on each re-`add`). + idempotency-test assertion.
- [Low] provider test defensively REDACTS the resolved key from a provider error message before
  surfacing it (the @relavium/llm adapters scrub too, but the key is in scope here — defense in depth),
  and no longer attaches the raw error as `cause` (it could carry the key into a `--verbose` render).
  The test now embeds the key in the provider error and asserts it's redacted to the hint.
- [Low] `provider add --base-url` is validated as a parseable http(s) URL (fail-fast), + a test. It is
  documented in commands.md as **recorded but not yet honored by request routing** — wiring a custom
  base URL to outbound requests lands later WITH the full SSRF base-URL gate (HTTPS-only;
  private/loopback/metadata blocked) per security-review.md, before any key is attached.
- [Low] withProviderDeps builds a single `createOsKeychainStore()` shared by the store-ref writes and
  the resolver (was two native instances per invocation).

db 17 provider-store + cli provider tests green; full gate green; Leakwatch 0; packages/core untouched.

Note: a security-review pass was blocked by a deny rule on apps/cli/src/secrets/; the other two reviews
read those files and verified the native-keychain confinement + stdin reader clean.

Refs: ADR-0019, ADR-0006, phase-2-cli.md 2.C
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cemililik, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements the relavium provider CLI command (workstream 2.C) for OS-keychain-backed API key management. Adds a ProviderStore to @relavium/db, defines a KeychainStore contract with @napi-rs/keyring implementation, stdin-only secret ingestion, keychain-first key resolver, five subcommand actions (list, add, set-key, remove-key, test), and wires everything into specs.ts. Extracts openLocalDb from history/open.ts as a shared DB initialization helper. Docs and roadmap updated to reflect completion of 2.H and advancement to 2.C.

Changes

Provider/Keys CLI Command (2.C)

Layer / File(s) Summary
Keychain contract, OS implementation, stdin reader, and build configuration
apps/cli/src/secrets/keychain.ts, apps/cli/src/secrets/os-keychain.ts, apps/cli/src/secrets/read-secret.ts, apps/cli/package.json, pnpm-workspace.yaml, .claude/settings.json
KeychainStore interface with get/set/delete, KeychainUnavailableError for backend failures, and keychainAccount formatter are defined. createOsKeychainStore wraps @napi-rs/keyring operations and translates errors to KeychainUnavailableError. readSecretFromStdin rejects TTY input and returns trimmed non-empty keys. @napi-rs/keyring added to CLI dependencies and workspace catalog. Claude deny-list updated to allow **/secrets/** access while blocking **/*.pem.
Provider registry store in @relavium/db
packages/db/src/provider-store.ts, packages/db/src/provider-store.test.ts, packages/db/src/index.ts
ProviderRecord, ProviderUpsert, and ProviderStore contracts define provider metadata persistence with keychain-ref tracking and validated defaultHeaders. createProviderStore implements list (sorted by name), get, upsert (idempotent by name), and keychain-ref lifecycle (setKeychainRef, clearKeychainRef). Raw key values never persisted. Full test suite validates idempotency, ref updates, timestamp semantics, error handling at read boundary, and sorted list output.
Local DB open/migrate/chmod helper and history refactor
apps/cli/src/db/open.ts, apps/cli/src/history/open.ts
openLocalDb(homeDir) consolidates config-dir creation, SQLite initialization, migrations, and 0600 chmod (including best-effort WAL/SHM sidecars) with idempotent close guarded by SQLite state. openHistoryStore simplified to delegate DB lifecycle and permission handling, removing inline chmod logic and simplifying imports.
Keychain-first key resolver and key hinting
apps/cli/src/engine/providers.ts
keyHint masks API keys, showing only the last 4 characters. createProviderResolver accepts optional keychain; keyFor resolution now tries keychain first (ignoring KeychainUnavailableError), falls back to env var, then throws invocation error naming CLI command or env-var path.
Provider command actions and dispatcher
apps/cli/src/commands/provider.ts
Implements list, add (HTTPS base-url validation), set-key (stdin read, keychain write, ref persistence), remove-key, and test (with raw-key redaction). Validation helpers enforce provider-name and HTTPS constraints. runProviderCommand dispatcher maps KeychainUnavailableError to CliError at boundary.
CLI specs registration and wiring
apps/cli/src/commands/specs.ts
Removes provider from stubs; adds registerProvider to define five subcommands with context-dependent dispatch. Introduces withProviderDeps to manage local DB, create keychain-backed resolver and store, wire stdin reader, and ensure DB closure. run command gains providers resolver dependency.
Provider command test suite
apps/cli/src/commands/provider.test.ts
Vitest coverage for all five command actions, key redaction in failures, base-url/provider validation, keychain-unavailable clean errors. Tests createProviderResolver.keyFor precedence (keychain→env→error), fallback paths, keyHint masking, and readSecretFromStdin TTY rejection.
Reference and roadmap documentation
docs/reference/cli/commands.md, docs/reference/contracts/config-spec.md, docs/reference/desktop/keychain-and-secrets.md, docs/roadmap/current.md, docs/roadmap/phases/phase-2-cli.md, CLAUDE.md, README.md
CLI command reference marks provider confirmed (no longer planned), documents subcommands and key-resolution precedence. Config spec and keychain docs clarify secrets.enc deferred past v1.0 and no silent plaintext fallback. Roadmap marks 2.H done (PR #44, ADR-0050) and updates "next pickup" to 2.C with revised build-order backbone.

Sequence Diagram

sequenceDiagram
  participant User
  participant CLI as relavium provider<br/>set-key
  participant withProviderDeps
  participant runProviderCommand
  participant readSecretFromStdin
  participant OsKeychainStore
  participant ProviderStore

  rect rgba(70, 130, 180, 0.5)
    note over User,ProviderStore: Provider set-key command flow
    User->>CLI: provider set-key openai
    CLI->>withProviderDeps: open DB, construct deps
    withProviderDeps->>OsKeychainStore: createOsKeychainStore()
    withProviderDeps->>ProviderStore: createProviderStore(db, uuid, now)
    withProviderDeps->>runProviderCommand: pass ProviderCommandDeps
    runProviderCommand->>readSecretFromStdin: readSecret()
    alt stdin not TTY
      readSecretFromStdin-->>runProviderCommand: trimmed key
      runProviderCommand->>OsKeychainStore: set(keychainAccount, key)
      runProviderCommand->>ProviderStore: upsert if missing
      runProviderCommand->>ProviderStore: setKeychainRef(name, account)
      runProviderCommand-->>CLI: keyHint(key) + exit success
    else stdin is TTY
      readSecretFromStdin-->>CLI: CliError(invalid_invocation)
    end
    CLI->>withProviderDeps: finally: close DB
  end

  rect rgba(100, 160, 100, 0.5)
    note over User,OsKeychainStore: Runtime key resolution (in relavium run)
    User->>CLI: relavium run workflow.yaml
    CLI->>OsKeychainStore: keyFor(providerId) via resolver
    alt keychain has entry
      OsKeychainStore-->>CLI: api key
    else keychain unavailable or empty
      CLI->>User: check RELAVIUM_PROVIDER_API_KEY env var
      alt env var set
        User-->>CLI: api key
      else env var missing
        CLI-->>User: CliError with config instructions
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • HodeTech/Relavium#44: Introduced durable run-history persistence in openHistoryStore; this PR directly refactors that by extracting shared openLocalDb helper and simplifying history store's DB lifecycle responsibility.

Poem

🐇 Hop to the keychain, secure and tight,
No plaintext keys in the CLI's sight!
OS keyrings guard each secret well,
While stdin pipes them with no TTY spell.
Provider commands land, 2.C takes flight,
A rabbit's security sprint shines bright! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing Phase 2.C provider/key commands with OS keychain integration via @napi-rs/keyring, which aligns with the substantial changes across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements workstream 2.C, introducing the relavium provider command to manage LLM providers and securely store their API keys in the OS keychain using @napi-rs/keyring. It adds database persistence for provider metadata, a key resolver that falls back to environment variables, and secure stdin reading for keys. Feedback on the changes highlights a potential UNIQUE constraint failed crash in the database upsert when a soft-deleted provider exists, an opportunity to improve error message extraction for non-Error objects in the provider test command, and a recommendation to make the database connection closure idempotent.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +97 to +124
const existing = activeRow(input.name);
if (existing === undefined) {
const row: NewLlmProviderRow = {
id: deps.uuid(),
name: input.name,
displayName: input.displayName,
baseUrl: input.baseUrl,
defaultHeaders: JSON.stringify(input.defaultHeaders ?? {}),
createdAt: t,
updatedAt: t,
};
db.insert(llmProviders).values(row).run();
} else {
db.update(llmProviders)
.set({
displayName: input.displayName,
baseUrl: input.baseUrl,
// `existing.defaultHeaders` is already the stored JSON STRING — keep it verbatim when the caller
// supplies none; stringify only a fresh value. (Re-stringifying the string would double-encode it.)
defaultHeaders:
input.defaultHeaders === undefined
? existing.defaultHeaders
: JSON.stringify(input.defaultHeaders),
updatedAt: t,
})
.where(eq(llmProviders.id, existing.id))
.run();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If a provider row with the same name was previously soft-deleted (i.e., deletedAt is not null), activeRow will return undefined. Consequently, upsert will attempt to insert a new row with the same name, which will fail with a UNIQUE constraint failed: llm_providers.name error and crash the CLI process. To prevent this, query any row (active or soft-deleted) by name, and if a soft-deleted one exists, reactivate it by setting deletedAt: null and updating its fields.

      const existing = db
        .select()
        .from(llmProviders)
        .where(eq(llmProviders.name, input.name))
        .get();
      if (existing === undefined) {
        const row: NewLlmProviderRow = {
          id: deps.uuid(),
          name: input.name,
          displayName: input.displayName,
          baseUrl: input.baseUrl,
          defaultHeaders: JSON.stringify(input.defaultHeaders ?? {}),
          createdAt: t,
          updatedAt: t,
        };
        db.insert(llmProviders).values(row).run();
      } else {
        db.update(llmProviders)
          .set({
            displayName: input.displayName,
            baseUrl: input.baseUrl,
            // `existing.defaultHeaders` is already the stored JSON STRING — keep it verbatim when the caller
            // supplies none; stringify only a fresh value. (Re-stringifying the string would double-encode it.)
            defaultHeaders:
              input.defaultHeaders === undefined
                ? existing.defaultHeaders
                : JSON.stringify(input.defaultHeaders),
            deletedAt: null,
            updatedAt: t,
          })
          .where(eq(llmProviders.id, existing.id))
          .run();
      }

// A clean failure. @relavium/llm already scrubs secrets from its error messages, but the key is in
// scope HERE, so defensively redact any occurrence before surfacing — and do NOT attach `err` as the
// cause (it could carry the key in a nested field that `--verbose` might render).
const raw = err instanceof Error ? err.message : String(err);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If err is a custom error object or a plain object with a message property (which is common for API/network errors), err instanceof Error will evaluate to false, and String(err) will return "[object Object]". To ensure robust error reporting and easier troubleshooting, check if err is an object containing a message property to extract a useful error message.

    const raw = err instanceof Error
      ? err.message
      : typeof err === 'object' && err !== null && 'message' in err
        ? String((err as { message: unknown }).message)
        : String(err);

Comment thread apps/cli/src/db/open.ts
Comment on lines +39 to +41
close: () => {
client.sqlite.close();
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make close() idempotent and prevent potential "Connection is already closed" errors if close() is called multiple times (e.g., in error recovery or cleanup paths), check if the database connection is open before attempting to close it.

    close: () => {
      if (client.sqlite.open) {
        client.sqlite.close();
      }
    },

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/cli/src/commands/provider.test.ts (1)

43-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid empty-object as casts in stubs.

{} as LlmProvider['supports'] and {} as Awaited<ReturnType<LlmProvider['generate']>> assert a shape the value doesn't actually have, which the repo's strict-TS rule disallows. Prefer a typed factory/fixture that constructs a minimally-valid object so the stubs stay sound if those types grow. As per coding guidelines: "No any, no unsafe as."

Also applies to: 66-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/commands/provider.test.ts` at line 43, Replace the unsafe `as`
cast `{} as LlmProvider['supports']` and similar casts (also at lines 66-68 with
`Awaited<ReturnType<LlmProvider['generate']>>`) with properly typed objects.
Instead of asserting empty objects to types they don't have, create a typed
factory function or fixture that returns a minimally-valid object satisfying the
required shape. This ensures type safety and alignment with the repo's strict
TypeScript rules that prohibit unsafe `as` casts.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/secrets/read-secret.ts`:
- Line 22: The stdin chunk handling in the chunks.push() call contains an unsafe
TypeScript cast using `as Uint8Array` which violates strict type checking rules.
Replace this cast with proper type narrowing using runtime checks instead. Check
the actual type of the chunk parameter at runtime (for example, using typeof or
instanceof) to determine whether it is a string or Uint8Array, and only then
pass it to Buffer.from() with the correct type, eliminating the need for any
cast.

In `@docs/reference/cli/commands.md`:
- Line 133: The phrase "Still landing in later workstreams:" creates a dangling
participial phrase without a clear grammatical subject. Fix this by rephrasing
it to include an explicit subject, such as changing it to "The following still
land in later workstreams:" or "Still landing in later workstreams are:" to
clarify what is actually landing in those future workstreams. Ensure the revised
text maintains the meaning while providing proper grammatical structure with a
clear subject for the participle.

---

Nitpick comments:
In `@apps/cli/src/commands/provider.test.ts`:
- Line 43: Replace the unsafe `as` cast `{} as LlmProvider['supports']` and
similar casts (also at lines 66-68 with
`Awaited<ReturnType<LlmProvider['generate']>>`) with properly typed objects.
Instead of asserting empty objects to types they don't have, create a typed
factory function or fixture that returns a minimally-valid object satisfying the
required shape. This ensures type safety and alignment with the repo's strict
TypeScript rules that prohibit unsafe `as` casts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4c0ceb7-ad79-4946-869a-7871ac98a679

📥 Commits

Reviewing files that changed from the base of the PR and between 597b53e and 8ae794c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • CLAUDE.md
  • README.md
  • apps/cli/package.json
  • apps/cli/src/commands/provider.test.ts
  • apps/cli/src/commands/provider.ts
  • apps/cli/src/commands/specs.ts
  • apps/cli/src/db/open.ts
  • apps/cli/src/engine/providers.ts
  • apps/cli/src/history/open.ts
  • apps/cli/src/secrets/keychain.ts
  • apps/cli/src/secrets/os-keychain.ts
  • apps/cli/src/secrets/read-secret.ts
  • docs/reference/cli/commands.md
  • docs/reference/contracts/config-spec.md
  • docs/reference/desktop/keychain-and-secrets.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-2-cli.md
  • packages/db/src/index.ts
  • packages/db/src/provider-store.test.ts
  • packages/db/src/provider-store.ts
  • pnpm-workspace.yaml

Comment thread apps/cli/src/secrets/read-secret.ts Outdated
Comment thread docs/reference/cli/commands.md Outdated
cemililik and others added 4 commits June 23, 2026 13:19
…grammar

Two valid findings from the external review (rest verified not-applicable, see below):

- openLocalDb's close() is now idempotent: guard on better-sqlite3's `.open` so a double close
  (e.g. an error-recovery path that also closes in a finally) is a no-op instead of throwing
  "database connection is not open". No current caller double-closes; this is defensive.
- commands.md: fixed the dangling participle "Still landing in later workstreams:" → "Two pieces
  still land in later workstreams:" (explicit subject).

Verified-and-skipped (no change):
- provider-store soft-deleted-row UNIQUE crash — N/A: idx_llm_providers_name is a PARTIAL unique
  index (WHERE deleted_at IS NULL), so a soft-deleted row doesn't occupy the name; and 2.C has no
  soft-delete path (remove-key clears the ref, keeps the row active). No crash is reachable.
- provider test err extraction (`String(err)` → "[object Object]") — N/A: @relavium/llm throws
  `LlmProviderError extends Error`, so `err.message` is always captured; the `String(err)` branch is
  an unreachable defensive fallback that the key-scrub still redacts. Kept the codebase-wide
  `err instanceof Error ? err.message : String(err)` convention.
- provider.test.ts `{} as ...` stub casts — left as-is: minimal test-double casts over complex seam
  types (CapabilityFlags, LlmResult) the test never reads; eslint permits them (necessary, not
  redundant). Building full fixtures adds coupling/noise without an assertion benefit.

Note: read-secret.ts (the `chunk as Uint8Array` narrowing finding) is under a workspace deny rule on
apps/cli/src/secrets/ — could not edit it; surfaced to the maintainer for a one-line apply or access.

cli typecheck/lint/test green; format clean; Leakwatch 0.

Refs: phase-2-cli.md 2.C
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d default_headers boundary, test+doc gaps

Four parallel review passes (standards/seam, security, correctness/tests, docs). Acted on the
still-valid findings; minimal changes.

Security / standards:
- provider add --base-url is now HTTPS-only (was http(s)): a plaintext `http:` endpoint can never be
  persisted, matching the routing-time gate (@relavium/llm assertHttpsBaseUrl, security-review.md
  §Network). The private/loopback/metadata range-block stays at routing time (it needs host resolution
  this fail-fast does not do); docs already record that forward obligation.
- provider-store fromRow no longer casts `JSON.parse(default_headers) as Record<string,string>` — it
  validates the shape at the DB read boundary (unknown + runtime check, no unsafe `as`; loud-on-corrupt,
  mirroring the sibling stores' JSON.parse posture). code-style-typescript.md §Boundaries.

Tests (guard logic the earlier fix-up commits touched):
- db: non-empty default_headers survives an update that omits them (the 8ae794c double-encode regression);
  createdAt preserved + updatedAt advances on update (was unobservable under the constant-clock stub);
  a corrupt default_headers value aborts the read loudly. (+3 → 103 pass)
- cli: the --base-url test now asserts `http://`, `file://`, and a malformed URL are all rejected (exit 2).

Docs (commands.md): --base-url "http(s)" → "HTTPS"; status stamp "(as of 2.F)" → "(as of 2.C)" to match
the rewritten body; Last updated 2026-06-03 → 2026-06-23; the keychain entry-naming scheme now links to
its canonical home (keychain-and-secrets.md#entry-naming) instead of restating service/account inline.

Verified-and-skipped: provider.test.ts `{} as ...` stub casts (test doubles over CapabilityFlags/LlmResult
whose fields the assertions never read; eslint permits them; real fixtures would couple tests to evolving
seam schemas) — left as-is. @napi-rs/keyring catalog `^1.1.6` vs resolved `1.3.0` drift — recommended as a
deliberate re-pin, not churned mid-review. read-secret.ts `chunk as Uint8Array` narrowing — still blocked by
the apps/cli/src/secrets/ deny rule; maintainer action (one-liner provided earlier).

Full gate green; format clean; engine-deps within allowlists; seam holds; Leakwatch 0; packages/core untouched.

Refs: ADR-0006, ADR-0019, security-review.md, phase-2-cli.md 2.C
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eyring ^1.3.0 re-pin

- read-secret.ts: narrow stdin chunks with `Buffer.isBuffer` / `typeof === 'string'` instead of the
  `chunk as Uint8Array` cast (CLAUDE.md §1 no unsafe `as`). An un-encoded stdin stream yields Buffers, so
  the real path is unchanged; any other shape is ignored rather than coerced. With the secrets/ deny lifted,
  a focused security pass re-audited the layer on disk and CERTIFIED it CLEAN (native-import confinement to
  os-keychain.ts, zero key/secret leakage, stdin-not-stdout TTY guard, canonical {providerId}:{keyId}
  naming, no hand-rolled crypto, sound chunk narrowing).
- @napi-rs/keyring catalog floor re-pinned ^1.1.6 → ^1.3.0 (the resolved/audited version) so the audited
  version is the shipped version for the most security-sensitive native dep — a deliberate re-pin, not a
  silent caret drift. Lockfile refreshed; binaries already at 1.3.0.

Full gate green; format clean; Leakwatch 0; seam holds; packages/core untouched.

Refs: ADR-0019, ADR-0006, security-review.md, phase-2-cli.md 2.C
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s stay reviewable

The blanket `Read(**/secrets/**)` deny blocked legitimate TypeScript source under any `src/secrets/`
module (it cost a security-review pass its read access to apps/cli/src/secrets/), without protecting any
real secret material — Relavium stores no secrets in-repo (OS-keychain only, ADR-0006), and actual secret
files are already covered by the `.env`, `.env.*`, and `**/*.pem` denies. Removing the over-broad rule lets
source-level security audits run; nothing secret becomes readable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== apps/cli/src/commands/provider.ts ===" && cat -n apps/cli/src/commands/provider.ts
echo ""
echo "=== apps/cli/src/commands/provider.test.ts ===" && cat -n apps/cli/src/commands/provider.test.ts
echo ""
echo "=== apps/cli/src/commands/specs.ts ===" && cat -n apps/cli/src/commands/specs.ts
echo ""
echo "=== apps/cli/src/secrets/keychain.ts ===" && cat -n apps/cli/src/secrets/keychain.ts
echo ""
echo "=== apps/cli/src/secrets/os-keychain.ts ===" && cat -n apps/cli/src/secrets/os-keychain.ts
echo ""
echo "=== apps/cli/src/secrets/read-secret.ts ===" && cat -n apps/cli/src/secrets/read-secret.ts
echo ""
echo "=== apps/cli/src/engine/providers.ts ===" && cat -n apps/cli/src/engine/providers.ts
echo ""
echo "=== apps/cli/src/db/open.ts ===" && cat -n apps/cli/src/db/open.ts
echo ""
echo "=== apps/cli/src/history/open.ts ===" && cat -n apps/cli/src/history/open.ts

Length of output: 49524


🏁 Script executed:

#!/bin/bash
echo "=== packages/db/src/provider-store.ts ===" && cat -n packages/db/src/provider-store.ts
echo ""
echo "=== packages/db/src/provider-store.test.ts ===" && cat -n packages/db/src/provider-store.test.ts
echo ""
echo "=== pnpm-workspace.yaml ===" && cat -n pnpm-workspace.yaml
echo ""
echo "=== .claude/settings.json ===" && cat -n .claude/settings.json
echo ""
echo "=== packages/db/src/index.ts ===" && cat -n packages/db/src/index.ts

Length of output: 25178



@cemililik: Completed a full review of all changes in PR #45. The implementation is solid — the security posture (stdin-only key intake, keychain-only storage, key redaction, HTTPS-only base-URL validation, env-var fallback), the DB abstraction, and the test coverage are all well-executed. A few findings below:


🔴 .claude/settings.json — Developer's local path leaked (Line 35)

A hardcoded absolute path from a developer's machine was committed:

"Read(//Users/dev/Documents/Projects/Agent-Organizer/**)"

This should not be in the repository — it exposes local filesystem structure and is almost certainly a leftover from personal Claude Code configuration. This line should be removed entirely.


🟡 apps/cli/src/commands/provider.ts Line 186 — requireHttpsUrl returns the raw (un-normalized) string

The function validates and then returns the original raw input rather than url.href. This means a user-supplied URL like HTTPS://API.EXAMPLE.COM/v1 passes the https: check but is stored with its original casing/path, rather than the normalized form. Returning url.href gives a canonical, consistent stored value:

// Before
return raw;

// After
return url.href;

Note: if you prefer to preserve the user's trailing-slash intent verbatim, this is a deliberate call — just worth documenting.


🟡 apps/cli/src/commands/provider.ts Lines 33 & 38 — Speculative test model names

anthropic: { testModel: 'claude-haiku-4-5' },
openai:    { testModel: 'gpt-5.4-mini' },

Neither claude-haiku-4-5 nor gpt-5.4-mini are current API model identifiers (as of the time of this review). If these don't resolve on the providers' APIs, relavium provider test will fail with an opaque 404/400 for end users. Consider using known-stable identifiers (e.g. claude-haiku-3-5, gpt-4o-mini) or documenting these as intentional forward-declarations that will break until the models ship.


🟢 apps/cli/src/commands/specs.ts Line 215 — Unnecessary lambda wrapper

Minor style nit:

// Before
readSecret: () => readSecretFromStdin(),

// After
readSecret: readSecretFromStdin,

Both are equivalent; the wrapper adds no behaviour.


✅ Everything else looks good

  • Key redaction (split(key).join(keyHint(key))) is correct and avoids the regex-escaping hazard of replaceAll with a literal that could contain . or *.
  • providerRemoveKey calling clearKeychainRef on an unregistered provider is safely a no-op — the UPDATE … WHERE name = ? AND deleted_at IS NULL matches zero rows.
  • openLocalDb close guard (client.sqlite.open check) correctly prevents double-close errors.
  • KeychainUnavailableError inheritance is fine at the ES2022+ target — no Object.setPrototypeOf needed.
  • withProviderDeps always closes the DB in finally — correct resource management.
  • upsert double-encoding fix (preserving the raw JSON string when defaultHeaders is omitted) is correctly implemented and tested.
  • readSecretFromStdin Buffer guard (Buffer.isBuffer) properly avoids unsafe casts.
  • history/open.ts refactor delegating to openLocalDb is clean and eliminates the duplicated permission logic.
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/cli/src/commands/provider.test.ts (1)

155-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test that asserts base-URL normalization.

This test currently checks “preserve base URL,” but not canonicalization. A normalization assertion would lock in the prior URL-format consistency fix and prevent regressions.

Suggested test addition
   it('set-key preserves a base URL set by a prior `add` (never clobbers it)', async () => {
     const d = deps({});
     await runProviderCommand(
       { action: 'add', name: 'openai', baseUrl: 'https://proxy.example/v1' },
       d,
     );
     await runProviderCommand({ action: 'set-key', name: 'openai' }, d);
     expect(d.store.get('openai')?.baseUrl).toBe('https://proxy.example/v1'); // not the SDK default
   });
+
+  it('normalizes --base-url before persisting', async () => {
+    const d = deps({});
+    await runProviderCommand(
+      { action: 'add', name: 'openai', baseUrl: 'HTTPS://PROXY.Example/v1' },
+      d,
+    );
+    expect(d.store.get('openai')?.baseUrl).toBe('https://proxy.example/v1');
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/commands/provider.test.ts` around lines 155 - 163, The test
'set-key preserves a base URL set by a prior add' currently only verifies that
the base URL is preserved, but does not assert that it is properly normalized or
canonicalized. Add an additional expectation to the test that verifies the
baseUrl stored in d.store.get('openai')?.baseUrl follows the expected normalized
URL format to lock in the URL canonicalization behavior and prevent future
regressions in URL formatting consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/commands/provider.test.ts`:
- Around line 236-244: The current code only captures and restores the value of
process.stdin.isTTY, which can leave the property descriptor mutated if the
original property was accessor-based. Replace the line that captures only the
value with Object.getOwnPropertyDescriptor to capture the entire original
property descriptor. Then in the finally block, instead of calling
Object.defineProperty with a hardcoded value, restore the complete original
descriptor using Object.defineProperty with the captured descriptor object to
ensure the property is fully restored to its original state after the
readSecretFromStdin test completes.
- Line 43: Replace all unsafe `as` type casts in the test file with proper type
guards or partial typed fixtures. For the `supports` property assignment (line
43), replace `{} as LlmProvider['supports']` with a `Partial<CapabilityFlags>`
that includes the required boolean properties (tools, streaming,
parallelToolCalls, vision, promptCache, reasoning) and media object structure.
For the two instances of `{} as Awaited<ReturnType<LlmProvider['generate']>>`
(lines 67 and 119), replace them with `Partial<LlmResult>` fixtures containing
appropriate test values. For the error assertions at lines 141-142, replace the
`(caught as Error).message` cast with a proper type guard using `instanceof
Error` or a property existence check before accessing the message property,
ensuring the code path only accesses message when the type is confirmed.

In `@apps/cli/src/db/open.ts`:
- Around line 23-37: The SQLite client connection opened by createClient(path)
at line 23 is not closed if runMigrations or any of the chmodSync operations
throw an error, causing a resource leak. Wrap the runMigrations call and the
subsequent chmodSync operations (including the for loop) in a try-finally block
where the finally block closes the client using client.db.close() if an error
occurs. Alternatively, use a try-catch block to catch any errors, close the
client before rethrowing the exception. This ensures the database handle is
always released even when setup fails.

In `@packages/db/src/provider-store.ts`:
- Around line 116-145: The upsert function has a race condition where the
check-then-act pattern (calling activeRow to read, then conditionally inserting)
allows concurrent processes to both observe a missing row and attempt to insert,
causing unique-constraint violations. Replace this check-then-act approach with
an atomic insert-with-conflict-handling operation that attempts to insert first
and handles conflicts by updating instead, using your database library's UPSERT
or INSERT...ON CONFLICT mechanism to ensure the operation is atomic and
idempotent-by-name behavior is guaranteed under concurrency.

---

Nitpick comments:
In `@apps/cli/src/commands/provider.test.ts`:
- Around line 155-163: The test 'set-key preserves a base URL set by a prior
add' currently only verifies that the base URL is preserved, but does not assert
that it is properly normalized or canonicalized. Add an additional expectation
to the test that verifies the baseUrl stored in d.store.get('openai')?.baseUrl
follows the expected normalized URL format to lock in the URL canonicalization
behavior and prevent future regressions in URL formatting consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a26f820-3f99-4526-87eb-a679cdee4c83

📥 Commits

Reviewing files that changed from the base of the PR and between 597b53e and 14fbe76.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • .claude/settings.json
  • CLAUDE.md
  • README.md
  • apps/cli/package.json
  • apps/cli/src/commands/provider.test.ts
  • apps/cli/src/commands/provider.ts
  • apps/cli/src/commands/specs.ts
  • apps/cli/src/db/open.ts
  • apps/cli/src/engine/providers.ts
  • apps/cli/src/history/open.ts
  • apps/cli/src/secrets/keychain.ts
  • apps/cli/src/secrets/os-keychain.ts
  • apps/cli/src/secrets/read-secret.ts
  • docs/reference/cli/commands.md
  • docs/reference/contracts/config-spec.md
  • docs/reference/desktop/keychain-and-secrets.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-2-cli.md
  • packages/db/src/index.ts
  • packages/db/src/provider-store.test.ts
  • packages/db/src/provider-store.ts
  • pnpm-workspace.yaml

stream: () => {
throw new Error('stream not used in provider test');
},
supports: {} as LlmProvider['supports'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find explicit casts in this test file.
rg -n --no-heading '\sas\s' apps/cli/src/commands/provider.test.ts

# 2) Locate LlmProvider type/interface declarations in-repo.
rg -n --no-heading 'interface\s+LlmProvider|type\s+LlmProvider' packages apps

# 3) Locate catch blocks and casted error assertions in this file.
rg -n --no-heading 'catch\s*\(|\(\s*caught\s+as\s+Error\s*\)' apps/cli/src/commands/provider.test.ts

Repository: HodeTech/Relavium

Length of output: 1596


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Read the LlmProvider interface definition
echo "=== LlmProvider Interface ==="
sed -n '472,530p' packages/llm/src/types.ts

# 2) Read the test context around the catch block (lines 130-150)
echo -e "\n=== Catch Block Context (lines 130-150) ==="
sed -n '130,150p' apps/cli/src/commands/provider.test.ts

# 3) Read the fixture context (lines 35-50)
echo -e "\n=== Fixture Context (lines 35-50) ==="
sed -n '35,50p' apps/cli/src/commands/provider.test.ts

# 4) Read context around line 67
echo -e "\n=== Context around line 67 ==="
sed -n '60,75p' apps/cli/src/commands/provider.test.ts

# 5) Read context around line 119
echo -e "\n=== Context around line 119 ==="
sed -n '110,130p' apps/cli/src/commands/provider.test.ts

Repository: HodeTech/Relavium

Length of output: 4531


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find CapabilityFlags definition
echo "=== CapabilityFlags Definition ==="
rg -n 'type CapabilityFlags|interface CapabilityFlags' packages llm

# 2) Find LlmResult definition
echo -e "\n=== LlmResult Definition ==="
rg -n 'type LlmResult|interface LlmResult' packages llm

# 3) Check if there are other test fixtures using these types
echo -e "\n=== Other test fixtures for LlmProvider ==="
rg -B2 -A4 'LlmProvider.*=' packages llm --type ts | head -40

Repository: HodeTech/Relavium

Length of output: 457


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Read CapabilityFlagsSchema definition
echo "=== CapabilityFlagsSchema Context (lines 220-240) ==="
sed -n '220,240p' packages/llm/src/types.ts

# 2) Search for LlmResult with proper path
echo -e "\n=== LlmResult Definition ==="
rg -n 'type LlmResult|interface LlmResult' --type ts packages/

# 3) Check what fields are actually needed for the test
echo -e "\n=== Checking test assertions on LlmResult ==="
rg -B5 -A5 'generate.*Promise.resolve' apps/cli/src/commands/provider.test.ts | head -30

Repository: HodeTech/Relavium

Length of output: 1111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Read LlmResultSchema definition
echo "=== LlmResultSchema Context (lines 330-350) ==="
sed -n '330,350p' packages/llm/src/types.ts

# 2) Read CapabilityFlagsSchema starting earlier
echo -e "\n=== CapabilityFlagsSchema Full Definition (lines 210-230) ==="
sed -n '210,230p' packages/llm/src/types.ts

# 3) Check what actual assertions are made on generate result in tests
echo -e "\n=== Actual test assertions on generate promise result ==="
rg -B10 'Promise.resolve.*as.*LlmProvider.*generate' apps/cli/src/commands/provider.test.ts | head -50

Repository: HodeTech/Relavium

Length of output: 2993


Remove unsafe as casts from test doubles and error assertions.

Lines 43, 67, 119, and 141-142 bypass strict typing with as casts:

  • Line 43: supports: {} as LlmProvider['supports'] — empty object cannot satisfy CapabilityFlags, which requires tools, streaming, parallelToolCalls, vision, promptCache, reasoning booleans and a complex nested media object with a refine constraint.
  • Lines 67, 119: {} as Awaited<ReturnType<LlmProvider['generate']>> — empty objects bypass LlmResult structure requirements.
  • Lines 141-142: (caught as Error).message — unsafe type assertion on a value declared as unknown. Use a type guard (instanceof Error) or property check instead.

Per coding guidelines, **/*.{ts,tsx} must use strict TypeScript with no unsafe as casts. Create properly typed test fixtures (e.g., Partial<CapabilityFlags>, Partial<LlmResult>) or use type guards for runtime checks.

🧰 Tools
🪛 ESLint

[error] 43-43: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/commands/provider.test.ts` at line 43, Replace all unsafe `as`
type casts in the test file with proper type guards or partial typed fixtures.
For the `supports` property assignment (line 43), replace `{} as
LlmProvider['supports']` with a `Partial<CapabilityFlags>` that includes the
required boolean properties (tools, streaming, parallelToolCalls, vision,
promptCache, reasoning) and media object structure. For the two instances of `{}
as Awaited<ReturnType<LlmProvider['generate']>>` (lines 67 and 119), replace
them with `Partial<LlmResult>` fixtures containing appropriate test values. For
the error assertions at lines 141-142, replace the `(caught as Error).message`
cast with a proper type guard using `instanceof Error` or a property existence
check before accessing the message property, ensuring the code path only
accesses message when the type is confirmed.

Sources: Coding guidelines, Linters/SAST tools

Comment thread apps/cli/src/commands/provider.test.ts Outdated
Comment thread apps/cli/src/db/open.ts
Comment on lines +116 to +145
upsert: (input) => {
const t = deps.now();
const existing = activeRow(input.name);
if (existing === undefined) {
const row: NewLlmProviderRow = {
id: deps.uuid(),
name: input.name,
displayName: input.displayName,
baseUrl: input.baseUrl,
defaultHeaders: JSON.stringify(input.defaultHeaders ?? {}),
createdAt: t,
updatedAt: t,
};
db.insert(llmProviders).values(row).run();
} else {
db.update(llmProviders)
.set({
displayName: input.displayName,
baseUrl: input.baseUrl,
// `existing.defaultHeaders` is already the stored JSON STRING — keep it verbatim when the caller
// supplies none; stringify only a fresh value. (Re-stringifying the string would double-encode it.)
defaultHeaders:
input.defaultHeaders === undefined
? existing.defaultHeaders
: JSON.stringify(input.defaultHeaders),
updatedAt: t,
})
.where(eq(llmProviders.id, existing.id))
.run();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make upsert atomic under concurrent callers.

Line 118 (read) + Line 120 (insert) is a check-then-act race. Two processes can both observe “missing” and one will fail with a unique-constraint error, so idempotent-by-name behavior is not guaranteed under concurrency.

💡 Suggested fix (insert-with-conflict handling, then update fallback)
+function sqliteErrorCode(err: unknown): string | undefined {
+  if (typeof err === 'object' && err !== null) {
+    const code = Reflect.get(err, 'code');
+    return typeof code === 'string' ? code : undefined;
+  }
+  return undefined;
+}
+
     upsert: (input) => {
       const t = deps.now();
-      const existing = activeRow(input.name);
-      if (existing === undefined) {
-        const row: NewLlmProviderRow = {
-          id: deps.uuid(),
-          name: input.name,
-          displayName: input.displayName,
-          baseUrl: input.baseUrl,
-          defaultHeaders: JSON.stringify(input.defaultHeaders ?? {}),
-          createdAt: t,
-          updatedAt: t,
-        };
-        db.insert(llmProviders).values(row).run();
-      } else {
-        db.update(llmProviders)
-          .set({
-            displayName: input.displayName,
-            baseUrl: input.baseUrl,
-            defaultHeaders:
-              input.defaultHeaders === undefined
-                ? existing.defaultHeaders
-                : JSON.stringify(input.defaultHeaders),
-            updatedAt: t,
-          })
-          .where(eq(llmProviders.id, existing.id))
-          .run();
-      }
+      const headersJson =
+        input.defaultHeaders === undefined ? undefined : JSON.stringify(input.defaultHeaders);
+      try {
+        const row: NewLlmProviderRow = {
+          id: deps.uuid(),
+          name: input.name,
+          displayName: input.displayName,
+          baseUrl: input.baseUrl,
+          defaultHeaders: headersJson ?? JSON.stringify({}),
+          createdAt: t,
+          updatedAt: t,
+        };
+        db.insert(llmProviders).values(row).run();
+      } catch (err) {
+        if (sqliteErrorCode(err) !== 'SQLITE_CONSTRAINT_UNIQUE') throw err;
+        const existing = activeRow(input.name);
+        if (existing === undefined) throw err;
+        db.update(llmProviders)
+          .set({
+            displayName: input.displayName,
+            baseUrl: input.baseUrl,
+            defaultHeaders: headersJson ?? existing.defaultHeaders,
+            updatedAt: t,
+          })
+          .where(eq(llmProviders.id, existing.id))
+          .run();
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/provider-store.ts` around lines 116 - 145, The upsert
function has a race condition where the check-then-act pattern (calling
activeRow to read, then conditionally inserting) allows concurrent processes to
both observe a missing row and attempt to insert, causing unique-constraint
violations. Replace this check-then-act approach with an atomic
insert-with-conflict-handling operation that attempts to insert first and
handles conflicts by updating instead, using your database library's UPSERT or
INSERT...ON CONFLICT mechanism to ensure the operation is atomic and
idempotent-by-name behavior is guaranteed under concurrency.

cemililik and others added 2 commits June 23, 2026 15:21
…st/typing follow-ups

Valid findings from the latest review (verified-and-skipped items below):

- db/open.ts: close the just-opened SQLite handle if runMigrations or the at-rest chmod throws
  (try/catch → close → rethrow). Previously a setup failure leaked the connection for the process
  lifetime. The post-return idempotent-close guard is unchanged.
- provider-store.ts: the two default_headers shape checks throw `TypeError` (a type/shape mismatch),
  not a bare `Error`. The corrupt-row test's message regex still matches.
- provider.test.ts: the stdin `isTTY` test now captures and restores the FULL property descriptor
  (getOwnPropertyDescriptor / defineProperty, or delete if it was absent) instead of a value-only
  restore that could leave a synthetic data property; the key-redaction assertions use an
  `instanceof Error` guard instead of `(caught as Error)`.
- specs.ts: `readSecret: readSecretFromStdin` (dropped the redundant lambda wrapper).
- provider.ts: documented that requireHttpsUrl stores the base URL verbatim (not url.href-normalized)
  to preserve the user's exact endpoint — a deliberate call; the routing-time gate re-parses it.

Verified-and-skipped:
- upsert "race condition" (atomic INSERT...ON CONFLICT) — local single-user CLI; the check-then-act
  fails SAFELY under the partial unique index (constraint error → non-zero exit, no corruption/leak),
  and a portable atomic upsert against a PARTIAL-index conflict target while preserving createdAt is
  disproportionate complexity for a scenario that does not occur on one machine.
- testModel ids (claude-haiku-4-5, gpt-5.4-mini, …) "don't resolve" — they DO: the project targets the
  4.x/5.x era (tech-stack.md / the model table); downgrading to 3.x/4o would be a regression.
- provider.test.ts supports/generate stub casts — test doubles whose fields the assertions never read;
  a real CapabilityFlags/LlmResult fixture must satisfy schema refines and would couple the test to the
  evolving seam for zero benefit (now documented in-code).

Full gate green; format clean; Leakwatch 0; seam holds; packages/core untouched.

Refs: ADR-0050, ADR-0006, phase-2-cli.md 2.C
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Read(//Users/dev/Documents/Projects/Agent-Organizer/**)` is one developer's local absolute path —
useless to any other contributor and it exposes the local filesystem layout in a tracked file. The
harness already permits reading within the working directory by default, so removing it changes nothing
functional.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/db/open.ts`:
- Line 27: The calls to runMigrations(client.db) and client.sqlite.close() are
flagged as unsafe due to unresolved type declarations from the `@relavium/db`
package. This occurs because `@relavium/db/package.json` references type exports
from ./dist/index.d.ts, but the dist directory has not been built yet. Build the
`@relavium/db` package to generate the necessary type declaration files before
importing and using its API in apps/cli. This will resolve the type safety
issues for both function calls and ensure strict TypeScript compliance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a5a11ae-43d4-421c-b2b2-1a6a762803a4

📥 Commits

Reviewing files that changed from the base of the PR and between 14fbe76 and d1eef7b.

📒 Files selected for processing (6)
  • .claude/settings.json
  • apps/cli/src/commands/provider.test.ts
  • apps/cli/src/commands/provider.ts
  • apps/cli/src/commands/specs.ts
  • apps/cli/src/db/open.ts
  • packages/db/src/provider-store.ts
✅ Files skipped from review due to trivial changes (1)
  • .claude/settings.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/cli/src/commands/provider.test.ts
  • packages/db/src/provider-store.ts
  • apps/cli/src/commands/provider.ts
  • apps/cli/src/commands/specs.ts

Comment thread apps/cli/src/db/open.ts
// If setup (migrations / the at-rest chmod) throws, close the just-opened handle before propagating —
// otherwise the SQLite connection leaks for the lifetime of the failing process.
try {
runMigrations(client.db);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether `@relavium/db` exports concrete types consumed by apps/cli.

set -euo pipefail

echo "== package metadata =="
cat packages/db/package.json
echo
fd -i 'tsconfig*.json' apps/cli packages/db --exec sh -c 'echo "### $1"; cat "$1"; echo' sh {}

echo "== public db exports =="
cat packages/db/src/index.ts
echo

echo "== createClient/runMigrations signatures =="
ast-grep outline packages/db/src/client.ts --view expanded

echo
echo "Expected: createClient and runMigrations have concrete exported types, and apps/cli resolves them without any/unknown fallthrough."

Repository: HodeTech/Relavium

Length of output: 7275


🏁 Script executed:

cat -n apps/cli/src/db/open.ts

Repository: HodeTech/Relavium

Length of output: 3071


🏁 Script executed:

# Check if there's a tsconfig issue with module resolution or declaration maps
cat -n apps/cli/tsconfig.json
cat -n packages/db/tsconfig.build.json

Repository: HodeTech/Relavium

Length of output: 980


🏁 Script executed:

# Verify the actual compiled .d.ts exports from `@relavium/db`
find packages/db/dist -name "*.d.ts" -type f | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -30 {}'

Repository: HodeTech/Relavium

Length of output: 207


🏁 Script executed:

# Check createClient implementation and return type annotation
ast-grep run --pattern 'export function createClient($_) : $RETURN_TYPE { $$$ }' --lang typescript packages/db/src/client.ts

Repository: HodeTech/Relavium

Length of output: 2716


🏁 Script executed:

# Check if ESLint actually reports errors at those lines in the CLI package
cd apps/cli && npm run lint 2>&1 | grep -A 5 -B 5 "open.ts" || echo "No lint errors in open.ts or linting passed"

Repository: HodeTech/Relavium

Length of output: 2621


🏁 Script executed:

# Check the full createClient implementation
sed -n '39,72p' packages/db/src/client.ts

Repository: HodeTech/Relavium

Length of output: 2051


🏁 Script executed:

# Check if `@relavium/db` needs to be built
ls -la packages/db/ | grep -E "(dist|build)"

Repository: HodeTech/Relavium

Length of output: 224


🏁 Script executed:

# Verify workspace configuration and package.json resolution
cat -n pnpm-workspace.yaml 2>/dev/null || cat -n package.json | grep -A 10 "workspaces"

Repository: HodeTech/Relavium

Length of output: 4067


🏁 Script executed:

# Check if there's a build script that generates types
grep -r "build\|typecheck" packages/db/package.json

Repository: HodeTech/Relavium

Length of output: 248


Fix unresolved @relavium/db typings at this boundary (unsafe calls on Lines 27 and 41).

runMigrations(client.db) and client.sqlite.close() are currently treated as unsafe, which indicates the imported DB API is crossing into this file with unresolved types. This happens because @relavium/db/package.json exports types from ./dist/index.d.ts, but the dist/ directory does not exist. Build the @relavium/db package before importing it in apps/cli to resolve the type declarations.

In strict TypeScript, unresolved imports can hide real runtime mistakes and must be eliminated.

🧰 Tools
🪛 ESLint

[error] 27-27: Unsafe call of a type that could not be resolved.

(@typescript-eslint/no-unsafe-call)


[error] 27-27: Unsafe member access .db on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/db/open.ts` at line 27, The calls to runMigrations(client.db)
and client.sqlite.close() are flagged as unsafe due to unresolved type
declarations from the `@relavium/db` package. This occurs because
`@relavium/db/package.json` references type exports from ./dist/index.d.ts, but
the dist directory has not been built yet. Build the `@relavium/db` package to
generate the necessary type declaration files before importing and using its API
in apps/cli. This will resolve the type safety issues for both function calls
and ensure strict TypeScript compliance.

Sources: Coding guidelines, Linters/SAST tools

@cemililik
cemililik merged commit 10e25b0 into main Jun 23, 2026
9 checks passed
@sonarqubecloud

Copy link
Copy Markdown

cemililik added a commit that referenced this pull request Jun 23, 2026
…xt pickup 2.E

PR #45 merged. Provider/key commands (the `relavium provider` registry + API keys in the OS keychain
via @napi-rs/keyring, resolved keychain → RELAVIUM_<PROVIDER>_API_KEY env var → error) are shipped.
No new ADR (secrets.enc deferred past v1.0; @napi-rs/keyring pre-authorized by ADR-0019).

- phase-2-cli.md: 2.C heading + status header + Remaining-build-order status line all marked ✅ Done
  (PR #45); the build-order table drops the 2.C row, renumbers (2.E now #1), and flips 2.C → ✓ in the
  MCP/chat blocker cells; the gate-closing backbone shrinks to `2.E → 2.G → 2.I → 2.L` (2.C joins
  2.K + 2.H as done).
- current.md: 2.C added to the Landed list (behind ADR-0019 + ADR-0006); next pickup → 2.E; date bump.
- CLAUDE.md + README.md: status paragraphs updated (2.C landed; next pickup 2.E).

Refs: phase-2-cli.md 2.C, ADR-0019, ADR-0006
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant