feat(cli): Sim CLI with AWS-style profiles and a platform key exchange - #6147
feat(cli): Sim CLI with AWS-style profiles and a platform key exchange#6147TheodoreSpeaks wants to merge 31 commits into
Conversation
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
…eat/sim-cli # Conflicts: # bun.lock
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
|
| 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
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- 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
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview The CLI auth exchange is extended beyond copilot-only keys: approvals record The browser Contract + generation groundwork: Reviewed by Cursor Bugbot for commit 89b4d9b. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryAdds 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.
Confidence Score: 5/5The pull request appears safe to merge. No blocking failure remains.
|
| 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
Reviews (6): Last reviewed commit: "fix(cli): review round 5 — header saniti..." | Re-trigger Greptile
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
…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
|
@cursor review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 681ee38. Configure here.
…sting them Merges `v2-api-spec` (#6150 — v2 endpoints for MCP servers, skills, custom tools, folders, credentials) and the newer `improvement/v2-endpoints`. ## The generator was list-driven, so none of it would have appeared `DOMAINS` and `SPEC_FILES` were hardcoded. Five new contract modules and a new `openapi-v2-resources.json` had landed, and the generator would have skipped every one — silently, with `--check` still passing, because the generated file matched a generator that never looked. Both are now discovered from disk. That is the same silent-drop class the review rounds kept surfacing, and it is the property the whole pipeline rests on: a new v2 domain should reach the CLI by regenerating, not by remembering to edit a list. Result: 47 → 72 operations, 13 contract modules, and 25 new commands (`sim skills list`, `sim mcp-servers get`, `sim folders delete`, …) with no CLI change beyond the discovery fix. Summaries for the new domains now resolve too, so their `--help` reads properly instead of falling back to `METHOD /path`. ## Confirmation gates for the new destructive operations Five new DELETEs arrived ungated. `deleteFolder` is the sharpest — the route archives the folder *and cascades to its contents* — so its message says so rather than reading like a single-item removal. Added a test asserting every DELETE carries a confirmation, with `undeployWorkflow` the one documented exception (reversible by redeploying). It fails against this commit's own starting state, so the next domain to arrive cannot land ungated the way these did. ## One fix outside the CLI `lib/skills/orchestration/skill-lifecycle.ts`, added by #6150, imports `OrchestrationErrorCode` from `@/lib/workflows/orchestration/types`, which does not exist — the type lives in `@/lib/core/orchestration/types`, where every other consumer reads it. The branch does not type-check without this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
…mains
`sim mcp-servers create` created the server, exited 0, and printed nothing.
The v2 route answers `{ data: { mcpServer: {...} } }`, and the record renderer
keeps only scalar fields — one key holding an object left it with none. Unwrap
a lone object-valued key before rendering; a payload with siblings (`{ row,
operation }` from upsert) is a real result and is left alone.
The five domains that arrived with the last generation had no contract columns,
so `mcp-servers list` inferred 20 including `hasOauthClientSecret`. Give each a
column set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
`sim workflows export <id>` printed `version` and `exportedAt` and nothing else. The record builder kept only scalar fields, so `workflow` and `state` — the entire export — were discarded with nothing to say they had been. Same for `workflows get`, which silently dropped `variables` and `inputs`. Record views now render every field. Nested values serialize to one line and are cut at 160 chars: visibly partial beats silently absent, and json/yaml output still prints them whole. Export is a document, not a record — it exists to be redirected to a file and fed back to `import`, and table/text flatten and truncate, so neither can round-trip it. `document: true` in the contract makes those formats fall back to JSON; yaml is honoured because it round-trips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
A workflow export is hundreds of lines, and `--workflow` only took it inline.
The shell makes that miserable: unquoted `$(cat wf.json)` word-splits into
broken JSON, and nothing in the help said passing a file was an option.
Every JSON flag now reads `@path`, or `@-` for stdin, so the round trip is
`sim workflows export <id> > wf.json` then `import --workflow @wf.json` — or
one pipe. `@` cannot collide with a real value because JSON only ever starts
with `{ [ " -`, a digit, or t/f/n.
Stdin drains with a readSync loop rather than readFileSync(0): a pipe is opened
non-blocking, so the single-read form returned EAGAIN and died with a raw stack
trace exactly when the upstream process had not written yet.
Parse failures that look like a filename now say so — naming @path, or the file
itself when the bare value turns out to exist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU
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
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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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.
`--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)}`) |

Adds
packages/sim-cli(@sim/cli, binsim) and extends the existing CLI key handoff so it can mint the credential the public API actually accepts.Based on
improvement/v2-endpointsrather than staging: the CLI's types are generated from the v2 Zod contracts, so it needs that surface to exist.Key exchange
The
/cli/authdevice flow already existed but only minted copilot keys, which do not authenticate/api/v1or/api/v2— those want a Sim platform key. The approval now carries ascope: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 otherwiseScope 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/orchestrationso the settings route and the exchange share one issuer.The CLI
Profiles work like the AWS CLI —
~/.sim/configfor settings ([profile dev]),~/.sim/credentialsfor keys at0600([dev]), selected with--profile/SIM_PROFILE. Each setting resolves flag → env → file → default, andsim whoamireports the winning source so a surprising value is explainable. CI can skip login entirely withSIM_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/yamlemit the API's raw values so switching format changes the encoding, never the data.Generation
scripts/generate-v2-cli-api.tsemitspackages/sim-cli/src/generated/v2-api.tsfrom 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-apifails CI when it is stale.packages/*must not importapps/*, so the generated file is plain type declarations with no imports; the script does the crossing at build time. It is piped throughbiome formatso the file is a fixed point of the formatter — lint-staged runsbiome check --writeon explicit paths, which bypassesfiles.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-deletevsdelete), flags where a field's type misdescribes its meaning (workflowIdsisz.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, andcheck:openapiall 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 loginround 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