Skip to content

feat(cli): Sim CLI with AWS-style profiles and a platform key exchange - #6147

Open
TheodoreSpeaks wants to merge 40 commits into
improvement/v2-endpointsfrom
feat/sim-cli
Open

feat(cli): Sim CLI with AWS-style profiles and a platform key exchange#6147
TheodoreSpeaks wants to merge 40 commits into
improvement/v2-endpointsfrom
feat/sim-cli

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Adds packages/sim-cli (@sim/cli, bin sim) and extends the existing CLI key handoff so it can mint the credential the public API actually accepts.

Based on improvement/v2-endpoints rather than staging: the CLI's types are generated from the v2 Zod contracts, so it needs that surface to exist.

Key exchange

The /cli/auth device flow already existed but only minted copilot keys, which do not authenticate /api/v1 or /api/v2 — those want a Sim platform key. The approval now carries a scope:

  • copilot (the default, so terminals built against the original flow are unaffected)
  • platform — a Sim API key: workspace-scoped when the approver is a workspace admin, personal otherwise

Scope and workspace are fixed at approval, not at poll. The poll is unauthenticated by necessity, so the browser is the only moment a human is present to consent and the only place a permission can be checked. The poll echoes back what was granted rather than what was asked for, so the CLI cannot file a copilot key under a platform profile and fail later with an unexplained 401.

Picking a workspace and scoping a key to it are deliberately separate. The terminal has no key yet, so it cannot list workspaces — the browser picker is the only place that choice can happen. The pick comes back as the profile's default whether or not the key is bound to it; otherwise a non-admin would choose a workspace by name and then have to go find its id by hand. Admin is required only to bind.

Personal-key creation moved into lib/api-key/orchestration so the settings route and the exchange share one issuer.

The CLI

Profiles work like the AWS CLI — ~/.sim/config for settings ([profile dev]), ~/.sim/credentials for keys at 0600 ([dev]), selected with --profile / SIM_PROFILE. Each setting resolves flag → env → file → default, and sim whoami reports the winning source so a surprising value is explainable. CI can skip login entirely with SIM_API_KEY + SIM_WORKSPACE, touching no files.

Commands cover workflows, logs, tables, files, and knowledge. Output format is a profile setting (table | json | yaml | text), not a per-command flag; json/yaml emit the API's raw values so switching format changes the encoding, never the data.

Generation

scripts/generate-v2-cli-api.ts emits packages/sim-cli/src/generated/v2-api.ts from the v2 contracts — types for all 47 operations plus an operation table (method, path, path params) the client dispatches through, so a route that moves or changes verb moves the CLI with it. check:cli-api fails CI when it is stale.

packages/* must not import apps/*, so the generated file is plain type declarations with no imports; the script does the crossing at build time. It is piped through biome format so the file is a fixed point of the formatter — lint-staged runs biome check --write on explicit paths, which bypasses files.includes, and would otherwise rewrite the generated file and fail its own check.

src/contract/ declares how the terminal maps onto the API. It is a diff against what is derivable, not a listing: 23 of the 47 operations need no entry. The 24 that do carry only what a schema cannot express — names where REST overloads a path (batch-delete vs delete), flags where a field's type misdescribes its meaning (workflowIds is z.string() the route splits on commas), output columns, and confirmation gates on destructive operations.

Not yet wired

The contract has no runtime consuming it, so the six command groups are still hand-written and the other 41 derived commands do not exist yet. That runtime is the next piece.

Verification

lint:check, type-check, check:api-validation:strict, check:cli-api, and check:openapi all pass. 55 CLI tests, plus server tests for the approve/poll routes, the approval store, and the browser approval view.

Not verified: the live sim login round trip end to end — it needs Redis plus a dev server for this branch. Every seam is unit-tested, but no real browser approval has run against it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj

TheodoreSpeaks and others added 13 commits July 30, 2026 10:59
Cherry-picks improvement/v2-endpoints (98c8567) onto the current base.

The v2 surface standardizes one response family across every endpoint:
`{ data }`, `{ data, nextCursor }`, and `{ error: { code, message, details? } }`,
rendered through apps/sim/app/api/v2/lib/response.ts. v1 auth and rate limiting
are reused as-is; the workspace-access and enterprise-audit checks are split into
`resolve*` cores returning structured failures, with thin v1 wrappers that render
the old `{ error }` body so v1 behavior is unchanged.

The branch's own /api/v2/tables/** is dropped. Staging's tables v2 (#6067,
typed predicate grammar + POST /api/v2/tables/[tableId]/query) supersedes it and
lands in the following merge; the two are reconciled onto the shared envelope
separately.

Conflict resolutions:
- v1/middleware.ts: keeps resolveWorkspaceRequestActor alongside the new
  resolveWorkspaceAccess/resolveWorkspaceScope split
- v1/audit-logs/auth.ts: keeps the newer targetOrganizationId parameter and
  isOrganizationBillingBlocked check inside the structured resolver
- bun.lock: taken from HEAD; the branch's lock churn was unrelated lucide-react
  hoisting

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
Brings the branch up to staging, including the tables v2 surface (#6067):
GET /api/v2/tables + POST /api/v2/tables/[tableId]/query, built on the typed
predicate grammar and feature-gated behind `tables-v2-api`.

Conflict resolutions:
- v1/audit-logs/auth.ts: staging's billing-off / AUDIT_LOGS_ENABLED entitlement
  path folded into the structured `resolveEnterpriseAuditAccess` resolver, so
  self-hosted deployments stay reachable on both v1 and v2
- apps/docs/openapi-v2-tables.json: staging's spec wins — it documents the
  shipped query surface, not the superseded rows/columns design. It was
  previously unwired; the multi-spec loader from the v2 pull-in now renders it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
Adds `packages/sim-cli` (`@sim/cli`, bin `sim`) and extends the existing CLI key
handoff so it can mint the credential the public API actually accepts.

## Key exchange

The handoff already existed but only minted *copilot* keys, which do not
authenticate `/api/v1` or `/api/v2` — those want a Sim platform key. The
approval now carries a `scope`:

- `copilot` (the default, so terminals built against the original flow are
  unaffected) mints as before
- `platform` mints a Sim API key: workspace-scoped when the approver is a
  workspace admin, personal otherwise

Scope and workspace are fixed at *approval*, not at poll: the poll is
unauthenticated by necessity, so the browser is the only moment a human is
present to consent and the only place a permission can be checked. The poll
echoes back what was granted rather than what was asked for, so the CLI cannot
file a copilot key under a platform profile and fail later with an opaque 401.

Picking a workspace and scoping a key to it are kept separate. The terminal has
no key yet, so it cannot list workspaces — the browser picker is the only place
that choice can be made, and the pick comes back as the profile's default
whether or not the key is bound to it. Otherwise a non-admin would pick a
workspace by name and then have to go find its id by hand.

Personal-key creation moves into `lib/api-key/orchestration` so the settings
route and the exchange share one issuer.

## CLI

Profiles work like the AWS CLI: `~/.sim/config` for settings (`[profile dev]`),
`~/.sim/credentials` for keys at 0600 (`[dev]`), selected via `--profile` /
`SIM_PROFILE`. Each setting resolves flag → env → file → default, and
`sim whoami` reports the winning source so a surprising value is explainable.
CI can skip login entirely with `SIM_API_KEY` + `SIM_WORKSPACE`.

Commands cover the v2 surface pulled in earlier: workflows, logs, files, and
knowledge, with `--output json` passing the API's own shapes through for `jq`.
`sim tables` is deliberately absent — that surface is still in flux.

## Drift fixes

The v2 routes were authored a month ago and had fallen behind their services:
`checkActorUsageLimits(userId, workspaceId)` → the billing-attribution flow
(which also restores correct payer attribution for workspace keys on KB upload
and search), `processDocumentsWithQueue` gained a required argument, and the
deploy/rollback param objects had stale fields. Caught by a cold type-check —
an incremental run had reported these files clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
Brings the branch's local work: the tables v2 surface realigned onto the v2
envelope, X-API-Key on usage-logs, and dedicated v2 billing/usage endpoints.
Also adds v2 workflow export/import.

Conflict resolutions — the other branch is further along on every shared file,
so it wins except where this branch is the only source:
- v2 lib/response.ts, knowledge routes, workflows deploy + contracts, and the
  OpenAPI specs: taken from improvement/v2-endpoints. It fixed the same service
  drift this branch did (billing attribution, deploy params), so the fixes are
  equivalent and theirs carries the newer surface alongside.
- v1/middleware.ts: the merge duplicated resolveWorkspaceAccess and
  checkWorkspaceScope, which both sides had added at different offsets. Dropped
  the duplicate block; the originals earlier in the file are unchanged.
- v1/audit-logs/auth.ts: doc-comment only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
…bles

The same endpoint was being described in three hand-maintained places: the Zod
contracts the routes validate against, the OpenAPI documents, and the CLI's own
TypeScript interfaces. Two of those are now derived.

## Generation

`scripts/generate-v2-cli-api.ts` reads `apps/sim/lib/api/contracts/v2/**` and
emits `packages/sim-cli/src/generated/v2-api.ts`: request/response types for all
44 operations plus an operation table (method, path, path params) the client
dispatches through, so a route that moves or changes verb moves the CLI with it.

The contracts are the right source because the routes validate against them — a
shape that disagrees with a contract is a shape the server would reject. Zod
4's `z.toJSONSchema()` handles all 110 schema slots; the JSON-Schema-to-TS
emitter is hand-rolled over that known-narrow subset and throws on anything
unrecognized rather than degrading to `any`, since silence is how a generated
client drifts.

`packages/*` must not import `apps/*`, so the generated file is plain type
declarations with no imports and the script does the crossing at build time.

`check:cli-api` fails CI when the file is stale. The generated directory is
excluded from biome: the pre-commit hook runs `check --write`, which would
otherwise reformat generated output and fail that check with an unrelated
message.

## OpenAPI: checked, not generated

The docs specs carry ~1000 hand-written descriptions and ~400 examples that Zod
schemas do not encode, so generating them would trade real documentation for
mechanical accuracy. `check:openapi-drift` reconciles structure instead — every
v2 path and method must exist on both sides — keeping the prose while still
failing on divergence. Both currently agree on all 44 operations.

## Tables

`sim tables list|get|columns|rows|insert|delete-rows`, built on the generated
types. Rows go through the POST query endpoint even unfiltered, since it is the
only shape carrying the predicate. Row columns are discovered at runtime and
unioned across the page, so a sparse row cannot hide a column.

Deletion requires an explicit `--row`/`--filter` selector *and* `--yes`; an
argument-less call would otherwise empty the table. Path params are
percent-encoded — an id containing `/` or `?` would otherwise retarget the
request.

The four existing command groups drop their hand-written interfaces for the
generated ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
The pre-commit hook rewrote the generated file immediately after it was
committed, so `check:cli-api` then failed in CI reporting contract drift that
had not happened — the only difference was quote style.

The biome.json exclusion added alongside it does not help: lint-staged runs
`biome check --write` on explicit paths, which bypasses `files.includes`. It
implied protection it never provided, so it is removed.

The generator now pipes its output through `biome format --stdin-file-path`
instead, making the emitted file conformant by construction. The hook has
nothing left to change, and the check compares like with like. A formatter
failure throws rather than emitting unformatted output, since falling back
silently would reopen the same loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
The picker fell back to "No workspace (personal key)" while the workspace query
was in flight, and Connect stayed live through that window. A fast click
approved a personal key with no default workspace — when the same click a moment
later would have issued a workspace-scoped key. The fallback read as an answer
rather than a pending state, so the card could promise one outcome and deliver
another.

Connect is now disabled until the list resolves, the trigger shows a loading
label (a placeholder would not show, since the fallback always counts as a
selection), and the explanatory line no longer asserts the personal-key outcome
before it is known.

Failure is treated as degraded rather than fatal: the picker disables but
Connect stays enabled and the copy says a personal key will be issued, so a
transient list failure cannot strand a waiting terminal.

Tests cover the pending, loaded, admin-binding, and error states; the two
loading assertions fail against the previous implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
A second login on the same day failed with `A workspace API key named "CLI
(2026-07-30)" already exists` — after the user had already approved in the
browser, so the whole handoff was wasted and there was no way to complete it
without renaming the existing key.

Key names are unique per owner, so the name has to be unique per login. Now
`CLI (2026-07-30 15:42:07Z)`: second precision, UTC so it is unambiguous in a
shared workspace key list and sorts chronologically.

The comment claiming a same-day collision was desirable (so logins would reuse
one key) was wrong — nothing reuses the key, the mint just fails. A collision at
second precision now means something genuinely unexpected, so it is still
surfaced rather than retried under a suffixed name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
Adds `packages/sim-cli/src/contract` — the declarative definition of how the
terminal maps onto the API — and folds in the v2 execution endpoints that just
landed on improvement/v2-endpoints.

## The contract

Read it as a diff against what is already derivable, not a listing. Method,
path, path params, field types, enum values, defaults and required-ness all come
from the generated operation table (which comes from the Zod contracts), and the
command name derives from `<resource> [sub-resource] <verb>`. 23 of 47
operations therefore need no entry at all.

The 24 that do carry only what a schema cannot express:
- names, where REST overloads one path — `DELETE /rows` vs `DELETE /rows/[rowId]`
  becomes `batch-delete` vs `delete`, and `DELETE /deploy` becomes `undeploy`
- flags, where a field's type misdescribes its meaning — `workflowIds` is
  `z.string()` that the route splits on commas; no generator can infer that
- columns, which are editorial
- confirm, for the 8 destructive operations

## Execution

`executeWorkflow` / `getWorkflowExecution` / `cancelWorkflowExecution` derive
badly (`/execute` and `/cancel` are verbs the deriver reads as nouns), so all
three are named explicitly: `workflows run`, `workflows executions get|cancel`.

`stream` is marked `omit`: it switches the response to SSE, which the JSON
client would try to parse. Advertising a flag that breaks the response is worse
than not offering it — a `--follow` command that renders the stream is separate
and hand-written, like `files download`.

## Also

- Drops `check:openapi-drift`. The branch landed `check:openapi`, which does the
  same path/method reconciliation plus a recursive field diff and validates doc
  examples against the real Zod schemas — mine was a strict subset.
- Surfaces the new v2 rollout gate in the CLI: it answers 404 for callers
  outside the cohort, indistinguishable from a missing resource, so a 404 now
  carries that as a possibility rather than a diagnosis.
- `executor/utils/errors.ts` widens instead of casting through `unknown`, which
  is both more honest (the value is an Error) and keeps the double-cast ratchet
  at 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
`--output` now takes table | json | yaml | text, settable per-command, via
SIM_OUTPUT, or persisted per profile as before.

`yaml` joins `json` in rendering the API's raw values rather than the table's
formatted cells, so a duration stays `1500` instead of becoming `"1.5s"` —
switching format changes the encoding, never the data. Line folding is disabled:
valid YAML, but it breaks line-oriented greps and is miserable to read.

`text` is tab-separated with no header and no colour — the shape `cut -f2` and
`while IFS=$'\t' read` expect, so shell plumbing works on a box with no JSON
tool. It uses the rendered cells rather than raw values, since it is a human-ish
format for pipelines rather than something to parse. An absent value collapses
to an empty field instead of the table's em-dash: `cut` returning a literal `—`
would read as a value to every downstream emptiness test.

A bad `--output` is now an error (commander `.choices`) rather than a silent
fall back to `table`. The environment variable and the config file stay tolerant
— those are ambient and set once, so a bad value should not break every command,
but a flag just typed should not be quietly disregarded.

Uses js-yaml 4.3.0, already a direct dependency of apps/sim, rather than adding
a second YAML library to the monorepo.

Also drops a stale README reference to check:openapi-drift, which the v2-endpoints
merge superseded with the deeper check:openapi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
Drops `-o, --output`. Format is set once per profile with
`sim configure --set-output <format>`, or overridden ambiently with SIM_OUTPUT
for a one-off (`SIM_OUTPUT=json sim logs list | jq`) and for CI, which already
runs file-less on env alone.

Both remaining sources are ambient — set once, then read by every later command
— so an unrecognized value falls back to `table` rather than breaking the CLI.
There is no longer a strict tier, because there is no longer anything typed
per-invocation to be strict about.

Frees `-o` for `sim files download -o <path>`, which previously had to share the
short flag with a global that meant something else entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
@TheodoreSpeaks
TheodoreSpeaks requested a review from a team as a code owner August 1, 2026 01:31
@gitguardian

gitguardian Bot commented Aug 1, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
35187658 Triggered Username Password 854f7d3 apps/desktop/src/main/browser-credentials/vault.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 4, 2026 6:23pm

Request Review

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes authentication and API key minting (approve/poll, workspace admin checks, credential storage) plus a large new CLI surface; regressions could strand logins or issue wrong key scopes.

Overview
Introduces @sim/cli (packages/sim-cli) with AWS-style profiles (~/.sim/config + credentials), sim login / configure / whoami, and v2 API commands for workflows, logs, tables, files, and knowledge. Types and an operation table are generated from v2 Zod contracts (generate:cli-api, check:cli-api in CI).

The existing CLI browser handoff is extended so approval can mint platform API keys (personal or workspace-bound), not only copilot keys. Scope, workspace pick, and admin-gated binding are fixed at approve (session + permissions); poll mints via shared performCreatePersonalApiKey / workspace orchestration and echoes granted scope/workspace. The /cli/auth UI adds a workspace picker (blocked until workspaces load) and URL params for scope / workspace hint.

Hand-written protocol commands cover binary file download/upload and table CSV import; the rest of the surface is generated or documented in README.

Reviewed by Cursor Bugbot for commit 7612e8d. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread packages/sim-cli/src/commands/auth.ts
Comment thread packages/sim-cli/src/commands/logs.ts Outdated
Comment thread packages/sim-cli/src/commands/auth.ts
Comment thread apps/sim/app/cli/auth/cli-auth-view.tsx Outdated
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds the Sim CLI and platform-key device authorization flow, including profile-based configuration, generated v2 API bindings, command execution and output rendering, while hardening downloads and terminal output.

  • Introduces @sim/cli with AWS-style profiles, authentication, HTTP dispatch, output formats, and command groups.
  • Extends browser approval and polling to issue copilot, personal platform, or workspace-bound platform keys.
  • Centralizes personal API-key issuance and adds generated v2 CLI contract validation to CI.
  • Fixes the previously reported overwrite, stream-finalization, and terminal-control handling paths.

Confidence Score: 5/5

The pull request appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/sim-cli/src/output/render.ts Adds table, JSON, YAML, and text rendering while comprehensively sanitizing API-derived display values and dynamic headers.
packages/sim-cli/src/commands/hand-written.ts Implements hand-written CLI operations with overwrite protection and controlled handling of open, write, and final-flush failures.
apps/sim/app/api/cli/auth/approve/route.ts Records the browser-approved key scope and workspace choice after enforcing membership and workspace-admin binding requirements.
apps/sim/app/api/cli/auth/poll/route.ts Mints the key type fixed at approval and returns the granted scope and workspace metadata to the CLI.
apps/sim/lib/api-key/orchestration/index.ts Centralizes personal API-key creation, conflict handling, auditing, and telemetry for settings and CLI callers.
packages/sim-cli/src/http/client.ts Provides authenticated API dispatch through the generated v2 operation metadata.
scripts/generate-v2-cli-api.ts Generates import-free CLI API types and operation metadata from the v2 route contracts.

Sequence Diagram

sequenceDiagram
  participant CLI
  participant Browser
  participant Approve as Approval API
  participant Store as Approval Store
  participant Poll as Poll API
  participant Keys as API Key Issuer

  CLI->>Browser: Open device authorization URL
  Browser->>Approve: Confirm scope and workspace
  Approve->>Approve: Validate session and permissions
  Approve->>Store: Save approved grant
  CLI->>Poll: Poll with request and verifier
  Poll->>Store: Redeem approved grant
  Poll->>Keys: Mint approved key type
  Keys-->>Poll: Return credential
  Poll-->>CLI: Return key and granted profile settings
Loading

Reviews (6): Last reviewed commit: "fix(cli): review round 5 — header saniti..." | Re-trigger Greptile

Comment thread packages/sim-cli/src/output/render.ts
Comment thread packages/sim-cli/src/commands/files.ts Outdated
Comment thread packages/sim-cli/src/commands/files.ts Outdated
Turns the CLI contract into working commands. 43 leaves across 7 groups, up
from the 6 hand-written ones — every v2 operation the contract does not hide is
now reachable, including `sim tables upsert`, `sim workflows run`, and the whole
tables surface.

## What the generator now emits

`V2_OPERATIONS` carries a field→slot map per operation: each query/body field's
kind, whether it is required, its enum values, and its server-side default.
Types alone could not drive this — the runtime has to *iterate* fields to build
flags, and everything from argv arrives as a string, so it needs the kind to
turn "50" into 50 and '{"a":1}' into an object.

It also lifts each operation's one-line `summary` from the OpenAPI specs. The
contracts carry validation, not prose, so `--help` had been showing raw URLs;
the specs already hold a written summary per operation and `check:openapi`
guarantees one exists, so this reuses documentation rather than inventing a
second place to describe the same endpoint.

## The runtime

`derive.ts` names a command `<resource> [sub-resource] <verb>` from the route,
covering 41 of 47. `request.ts` assembles the call: path params from positional
args, `workspaceId` injected from the profile into whichever slot declares it,
everything else coerced and validated locally — so a bad enum, malformed JSON,
missing required flag, or absent workspace fails before any network call.
`build.ts` constructs the commander tree, auto-pages cursor lists up to
`--limit` (0 for everything), and renders through the contract's columns or, for
runtime-shaped rows, keys unioned across the page.

Fixed while wiring: `new Command('upsert <tableId>')` makes the *whole string*
the command name, so `sim tables upsert` never matched and fell through to the
group's help. Arguments have to be declared with `.argument()`.

## What stays hand-written

Two leaves, each for a reason generation cannot satisfy in principle:
`files download` streams binary rather than the JSON envelope, and
`tables rows list` discovers columns from user-defined row data nested under
`data`. They attach onto the generated groups, so `sim files --help` lists them
alongside the rest. The five previous command files are deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
Comment thread packages/sim-cli/src/runtime/build.ts Outdated
…afety

## CLI flags silently dropped (Cursor, High)

Commander camelCases every multi-word flag, so `--min-duration-ms` is stored as
`minDurationMs`. `buildRequest` looked flags up by their own kebab name, found
nothing, and dropped the field — no error, it just never reached the API. That
was every multi-word flag on every generated command.

The unit tests passed because they fed flag values already keyed by flag name,
which is not what commander produces — they validated a fiction. Added
`build.test.ts`, which parses real argv through the built commands; three of its
assertions fail against the previous code. The old tests now use camelCase keys
with a comment saying why.

## Terminal control sequences (Greptile, P1 security)

`stripAnsi` matched only SGR (`ESC [ … m`), so a knowledge document, table cell,
or workflow name could carry OSC, non-SGR CSI, or `ESC c` through to an
interactive terminal — setting the window title, moving the cursor to overwrite
what was already printed, or resetting the terminal. Replaced with a `sanitize`
covering OSC (BEL- and ST-terminated), CSI, any ESC + printable, and the bare
C0/C1 range, keeping tab and newline. Applied where API values become display
text, so the colour the CLI adds afterwards still works.

## Downloads (Greptile, P1 ×2)

`createWriteStream` truncated silently, and the destination name usually comes
from the server's content-disposition rather than anything the caller typed —
so a download could irreversibly replace an unrelated local file. Now opens `wx`
and fails with a message naming `--force`, which was added for the deliberate
overwrite.

The stream's error listener was attached after the read loop finished, so an
EEXIST/EACCES/ENOSPC during writing was an unhandled 'error' event that took
down the process. It is now registered before the first write and raced against
the pump.

## Personal-key caption (Cursor, Low)

With "No workspace (personal key)" picked, the caption still promised a default
workspace the approval does not send. It now distinguishes no-pick from
picked-but-not-admin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread packages/sim-cli/src/runtime/build.ts Outdated
Comment thread packages/sim-cli/src/output/render.ts Outdated
Comment thread packages/sim-cli/src/contract/commands.ts
TheodoreSpeaks and others added 2 commits August 1, 2026 11:22
Regeneration picked up seven new operations (72 → 79), every one of which
derived badly. `/files/move` and `/files/bulk-archive` put a verb where the
deriver expects a sub-resource, so each became a group holding a lone `create`;
`GET /files/[id]/share` fetches one share and was read as a collection and
named `list`; and `PATCH /files/[id]` derived to `files update` while its own
summary said "Rename File".

Named them: batch-archive (matching tables rows batch-delete), move, rename,
restore, set-content, share get, share set. Bulk archive is gated behind --yes
like the other batch destructives.

`files list` gained --scope active|archived, and its rows now carry folderPath —
added as a column, since which folder a file sits in is what distinguishes two
rows sharing a name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
Comment thread packages/sim-cli/src/runtime/build.ts Outdated
The counterpart to `files download`, and hand-written for the same reason:
POST /api/v2/files is multipart, which the generated flag surface cannot
express, so `uploadFile` has been hidden since the start.

Reads the file with openAsBlob so it stays on disk while the request is
written, rather than buffering the whole upload in memory. Size is checked
against the route's own 100MB ceiling before anything is sent. Content type
comes from the extension, since the stored type decides whether the workspace
later renders a file or offers it for download.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
Three things stacked up so the command appeared to do nothing.

A row's cells live under `data`, and column inference skips object-valued
fields — so the table came back listing an id and two timestamps per row and
none of the content the query was run for. `expand` names the wrapper whose
keys become columns, unioned across the page like the top-level ones. A cell
key that shadows a top-level field is shown by its full path, so two different
values never share a header.

A cell containing a newline pushed the rest of its row onto the next line and
every column after it lost alignment; in text mode a tab invented a field that
`cut -f` reads as real. Display cells are now flattened to one line. `sanitize`
still keeps \t and \n — json and yaml must round-trip them, and this is applied
only to finished cells.

A single cell holding an LLM response set the column width for the whole table
and pushed everything after it off-screen, so table cells clamp at 60 columns.
text/json/yaml are untouched: those exist for the whole value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b97987e. Configure here.

Comment thread packages/sim-cli/src/runtime/build.ts Outdated
`--is-active false` turned sharing ON and reported success. Booleans were
declared presence-only, so the flag meant `true` and commander dropped the
`false` as an argument the command had no use for — silently, because excess
arguments are ignored by default.

A required boolean now takes its value (`--is-active <true|false>`): it is a
state to set, not a switch to flip on, and as a presence flag it could only
ever send one of the two values it needs to express.

Optional booleans stay presence-flags — `--deployed-only` reads better than
`--deployed-only true` — but each also gets `--no-<name>`. Omitting one means
"leave it alone", which is not the same as setting it false; without the
negation there was no way to disable an MCP server or unlock a folder.

Excess arguments are now an error on every generated command, so a value
attached to the wrong flag stops rather than being silently discarded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
export function printRecord(format: OutputFormat, fields: Array<[string, string]>, raw: unknown) {
const machine = renderMachine(format, raw)
if (machine !== null) {
console.log(machine)

if (format === 'text') {
for (const [label, value] of fields) {
console.log(`${label}\t${oneLine(stripAnsi(value))}`)

const width = Math.max(...fields.map(([label]) => label.length))
for (const [label, value] of fields) {
console.log(`${chalk.dim(pad(`${label}:`, width + 1))} ${oneLine(value)}`)
TheodoreSpeaks and others added 2 commits August 3, 2026 18:00
79 → 111 operations across three merged PRs.

The generator could not read the new contracts at all: a table view's filter is
a recursive predicate, so Zod lifts it into `$defs` and refers to it, and
`toTypeScript` threw on the first `$ref`. Those definitions are now hoisted into
named aliases — recursion TypeScript resolves without complaint — named after
the type that owns them so two operations lifting their own `__schema0` cannot
collide.

Uploading is no longer one multipart POST. `POST /api/v2/files` is gone,
replaced by a presigned handshake, so `files upload` was left calling a route
that no longer exists. It now creates the upload, signs part URLs in batches of
100 (each is short-lived, so signing all of them up front would expire the last
ones), PUTs each part straight to storage, and completes with the ETags —
aborting the upload if any step fails, since a half-finished one holds storage.
Parts are read through `Blob.slice`, so only the part in flight is in memory.
Verified byte-identical on a 24MB round trip.

The rest is naming. `/cancel-runs`, `/rows/find`, `/restore`, `/columns/run`
and the enrichment path each put a verb where a sub-resource was expected, so
each had become a group holding a lone `create`. Transfer steps keep names that
say what they are, since no single command drives a table import yet.

Three new DELETEs needed gates, which the existing guard test caught. Aborting
an upload and cancelling an import or export stop something in flight rather
than destroying something kept, so those are exempt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
Imports a CSV into a new or existing table, driving the same presigned
handshake `files upload` uses — the two are the same protocol against
different paths, so they now share one implementation.

What made this more than a wrapper is that the import carries decisions the
handshake does not: the source is a local file or one already in the workspace,
the target is a new table or an existing one to append to or replace, and
mapping/createColumns are rejected unless the target is existing. Both choices
are required rather than inferred — defaulting to a new table would turn a
forgotten --to-table into a silent second copy of the data — and the
conditional flags are checked here so the error names the flag instead of
arriving as a complaint about the request body.

The transfer only queues the work; rows are parsed afterwards, so returning at
`complete` would report success for an import that goes on to fail on a bad
row. It polls to a settled status and reports the rows written, with progress
on a terminal only. --no-wait opts out.

The handshake steps are hidden now that a command drives them; `imports get`
and `imports cancel` stay, being useful against an import already running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
`sim tables import people.csv` now does the obvious thing rather than
demanding a target. Requiring one guarded the wrong direction: a forgotten
flag creating a new table is visible and easily undone, while the outcome
worth protecting — writing into an existing table — is the one that now has
to be asked for by name.

--to-table becomes --table-id, and --mode/--mapping/--create-columns apply
only alongside it. Passing one without it is an error rather than a no-op:
silently ignoring `--mode replace` would let it read as honoured while a new
table was created beside the one it was meant to overwrite. The reverse is
also refused, since --table-id already names the destination.

The derived name is sanitized, because table names are identifiers: the
obvious basename would reject most real files, so `2026-quarterly sales.csv`
imports as `_2026_quarterly_sales` instead of failing. --name overrides it,
and is required for --file-id, where there is no file name to take one from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
TheodoreSpeaks and others added 3 commits August 3, 2026 20:35
`listTables` gained `limit` and `cursor`, so the CLI's auto-pager now drives it
like every other paginated list — no CLI change, which is the point of
generating this file. Also picks up `isCurrent` on workflow versions and a new
`voice-output` enum member.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
Both were dead on arrival, for opposite reasons.

`createTableRows` takes `z.union([batch, single])`. A union has no flat field
list, so the generator emitted no body slot — and slot absence reads the same
as "this operation has no body", so the command offered nothing and sent
nothing. The generator's own comment claimed the runtime fell back to taking
the body as JSON; nothing did. Unions are now marked, the fields every branch
shares are still emitted (both require `workspaceId`, which comes from the
profile), and `--body <json|@file>` carries the rest, merged over them so the
caller still wins on any key it sets. Dropping that merge was my first attempt
and it failed on the missing workspace.

`runTableColumn` takes `limit: { type, max }`. The pager claimed the *name*
`limit` regardless of type, so it became `--limit <n>` with a default of 100
and sent a number the route rejected on every call, whether or not the flag was
passed. The special case now applies only where `limit` is numeric; elsewhere
it is an ordinary field and gets the JSON flag its type calls for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
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.

2 participants