Skip to content

feat(cli): build apps with the agent from the terminal — base44 builder + base44 code - #630

Open
ayal wants to merge 6 commits into
mainfrom
terminal-t1
Open

ayal wants to merge 6 commits into
mainfrom
terminal-t1

Conversation

@ayal

@ayal ayal commented Sep 17, 2026

Copy link
Copy Markdown

What

Build Base44 apps from the terminal. This adds five non-interactive base44 builder commands (create, send a turn, status, stop, pick the model), base44 sandbox preview, and an interactive base44 code session, all built on one shared resource layer. Everything hits existing backend endpoints — no backend changes.

What it looks like

base44 code in an empty directory, before the first prompt — the rendered mark, the account, the directory, the kind of app:

base44 code — idle

The same session a few minutes into the first build — tool results as they land, the agent's current thought, the tool in flight, and the footer chips:

base44 code — building

Layout

packages/cli/src/
├── core/resources/apps/          # shared by both surfaces
│   ├── api.ts                    #   createApp · createImportedApp · sendTurn · getAppState
│   │                             #   getPreviewUrl · stopTurn · resolveActiveBranchId · GitHub re-auth
│   └── stream.ts                 #   poll full-conversation, diff → events
├── core/model.ts                 # model catalog · read/save the account's pick
└── cli/commands/
    ├── builder/                  # non-interactive atoms
    │   ├── index.ts              #   `builder` group
    │   ├── new.ts · send.ts · status.ts · stop.ts · model.ts
    │   └── shared.ts             #   createAndLinkApp · assertBuilderApp · appTypeChip · resolveBranchId · githubReauthLines
    ├── sandbox/preview.ts        # `sandbox preview` — the app's preview URL (joins the existing sandbox group)
    └── code/                     # interactive session; builds on builder/shared.ts
        ├── index.ts              #   `code` command (create on first prompt | reopen | --app-id)
        ├── session.tsx · session-engine.ts · render.ts · paste.ts

program.ts registers builder and code; the existing sandbox group gains preview. base44-client.ts adds one request header, X-Builder-Model-Selection: user-v1, so the backend honors the saved model pick (the web editor's contract; with no pick it falls back to the app default).

Command reference

Every command and flag this PR adds. Globals from the existing CLI apply where marked: --json (machine output on stdout), --app-id <id> (also BASE44_APP_ID; otherwise the linked directory), --branch <name> where a command is branch-aware.

Command Arguments / flags Globals
base44 builder new [prompt] --import <repo> build over an existing GitHub repository · --mode <direct|fork|copy> how to import (default direct) · --repo-name <name> name for the new repo when forking/copying · --from-branch <name> import a specific branch · --name <name> directory and app name (invented when omitted) · --path <dir> directory to link (default: the current directory when empty, else ./<name>) · --wix-instance <token> create through the Wix route with this signed instance (- reads it from stdin; also BASE44_WIX_INSTANCE) · --wix-client-id <id> the companion OAuth app's client id · --auto-approve / --skip-questions policy for the first build · --verbose every tool result unfolded · --stream-json one JSON line per stream event, then result --json
base44 builder send [message] one agent turn; blocks until it finishes; {"queued":true} when the agent is busy · or answer what the agent asked: --approve · --reject · --skip · --choose <label> (repeat per question, comma-separate for multi-select) · --other <text> · --grant <keys> · --secret NAME=env:VAR|file:PATH|- (never plain text) · --connector-name <name> [--client-id, --client-secret <source>] for a workspace connector · --input <json> · --id <call> when several wait · policy: --auto-approve, --skip-questions · --verbose · --stream-json one JSON line per stream event (thinking, text, tool_start, tool_end with full args and result, waiting with the request), then result --json · --app-id · --branch
base44 builder status building, ready, errored — or waiting, with what the agent asked --json · --app-id · --branch
base44 builder stop stop the running turn, server-side --app-id · --branch
base44 builder model [model] list the catalog with the current pick, or set it by name or id (Automatic/default/auto clears the pick) --json
base44 sandbox preview the live preview URL (cold-starts the sandbox) --json · --app-id
base44 code interactive session · --import <repo> · --path <dir> · --wix-instance <token> [--wix-client-id <id>] · --app-id <id> opens that app from anywhere

Inside base44 code: Enter sends · Esc stops the running turn, or snaps back to live when scrolled · ↑/↓ · PgUp/PgDn scroll the transcript · Ctrl+O unfolds every tool result in the whole transcript, and folds them again · Tab reopens a question card you deferred · Ctrl+C clears the input, then exits · Ctrl+D exits · /model opens the picker, /model <name> switches directly.

When the agent asks you something. The builder parks a tool call with waiting_for_user_input when it needs a decision — enable a connector, write or delete entity records, run a guarded shell command, answer clarifying questions, grant an app agent permissions, provide secrets, or finish an OAuth step. The session shows a card and answers through POST chat/submit-tool-call-input, the route the editor uses, then streams the turn the answer resumes. Approvals: y / n. Questions: an arrow picker, space for multi-select, a "something else" row for free text, s to skip them all. Permissions: checkbox rows. Workspace-connector registration: a name, then Base44's credentials or your own client id and secret. Secrets: masked entry field by field; values exist only until the answer is sent and never appear in the transcript, a log, telemetry or --stream-json. Browser steps (GitHub, OAuth, payments): finish in the editor, then y. Esc defers a card; Tab brings it back. OAuth steps (connector authorization, GitHub connect) run from the card the way the editor runs them in a popup: the CLI starts the flow, prints and opens the link, polls the connection, and approves the tool call only once it is active. A parked tool this CLI cannot render points at the editor instead of offering a misleading yes/no.

Non-interactive: the next send is the answer. Modelled on claude -p, where a question comes back as the result and the next message answers it. When the builder parks a call, send, new and status return status: "waiting" with pending — the same shape the cards render from — and --stream-json waiting events carry it too. The caller answers with send --approve / --reject / --choose / --grant / --secret NAME=env:VAR / --skip / --input, validated against what was asked, and the resumed turn streams like any message. A message sent while a call is parked is refused with the pending list rather than a bare 4xx. Two policy flags for scripts, scoped to the kinds where a fixed policy makes sense: --auto-approve (approval-kind pauses, all requested permissions) and --skip-questions (the builder decides). Secrets, browser steps and unknown forms never have a policy: they always come back as waiting.

$ base44 builder send "add a store" --json
{"status":"waiting","error_source":null,"reply":null,"pending":[{"id":"tc_1","kind":"approval","tool":"enable_connector","title":"Enable wix_stores?","detail":"Sell the pillows through Wix Stores"}]}
$ base44 builder send --approve --json
{"status":"ready","error_source":null,"reply":"Wix Stores is connected; products page added."}

What a tool shows. While it runs, the tool sits in place in the transcript with a pulsing dot, its title, elapsed time and its command or path beneath; the finished line replaces it. An edit renders as the change itself, removed lines then inserted lines, folded past 12; a write shows +N lines; reads and reloads stay quiet on success; any other successful result shows one line with an … +N lines marker; errors are never cut mid-sentence, whole lines folded past 8. The stream carries every tool's full result and parsed arguments, so the fold is a rendering choice, not a data loss — --verbose (or Ctrl+O) shows all of it, and --stream-json hands it over as data.

builder send and code refuse a code-first project (base44 create) and a Superagent; --mode, --repo-name and --from-branch are refused without --import; --import and --path are refused inside a linked directory.

Non-interactive — base44 builder

Where the app lands (same rule as base44 create): an empty current directory is the project itself and names the app; otherwise ./<name>/ is created. --path <dir> picks the directory explicitly. builder new and a genesis base44 code session share this, and both end by printing the directory and the cd … && base44 code line to pick the app up again. The block also says the app's files live remotely (base44 sandbox ls to look, base44 eject for a copy) — a linked directory is a handle, like base44 link, not a checkout.

Wix launch. A Wix-launched app is born through POST /api/wix/create-app, which verifies the signed Wix instance and connects the Wix connector before the first turn — the one thing POST /api/apps cannot do. --wix-instance <token> takes that signed instance and --wix-client-id <id> the optional companion OAuth app; the prompt argument is what the agent builds. In the browser the same two values travel in the launch URL's fragment; the CLI takes them as flags, so the funnel prints them and nothing parses URLs. The token is a credential, so - reads it from stdin and BASE44_WIX_INSTANCE is honoured, and neither the request body nor the option value reaches error telemetry. Everything after creation is the shared path: link, stream, --json / --stream-json, plus client_creation_id in the result. The route checks the instance against the backend's own Wix app secret, so the target environment must hold the credentials of the Wix app that minted it.

Each command is a Base44Command, so it gets the existing plumbing for free: --app-id <id> / linked-dir resolution, --branch, and the global --json.

Only builder-managed apps. builder send (and code) first read the app and refuse two kinds with a one-line message: a code-first project (base44 create, is_managed_source_code: false, where you own the source and a turn would edit a copy nobody sees) and a Superagent (which has no builder conversation).

builder new — create an app and start its first build. A prompt makes a template app; --import <repo> builds over an existing GitHub repo.

$ base44 builder new "invoice tracker for freelancers" --json
{"id":"a1b2c3","repo_url":null,"editor_url":"https://app.base44.com/apps/a1b2c3/editor/preview",
 "preview_url":"https://preview-a1b2c3.base44.app","status":"ready"}

$ base44 builder new --import https://github.com/me/my-store --mode direct --json
{"id":"d4e5f6","repo_url":"https://github.com/me/my-store", … ,"status":"created"}

Without --json it shows a live spinner while the sandbox provisions, then streams the agent's tool calls, prints repo / editor / linked ./<dir>, then the preview URL. Flags: --import <repo>, --mode direct|fork|copy, --name, --repo-name, --from-branch. Inputs are validated before any API call.

builder send — one agent turn; blocks until it finishes and streams it.

$ base44 builder send "add a dark-mode toggle to the header"
  ↳ read_file   src/components/Header.jsx
  ↳ write_file  src/components/Header.jsx
✓ Turn finished.

$ base44 builder send "" --json
{"status":"ready","error_source":null,"reply":"Added a dark-mode toggle…"}
# a busy agent → {"queued":true}

builder status · sandbox preview · builder stop

$ base44 builder status --json     {"id":"a1b2c3","state":"ready","message":null}
$ base44 sandbox preview --json    {"preview_url":"https://preview-a1b2c3.base44.app"}   # boots the sandbox if cold
$ base44 builder stop --json       {"stopped":true}                                       # server-side stop

builder model — pick the builder model for your turns, account-wide. No argument lists the catalog and the current pick; default clears it.

$ base44 builder model --json
{"current":"claude_opus_5","models":[{"name":"default","id":null},{"name":"Opus 5","id":"claude_opus_5"}, …]}
$ base44 builder model sonnet
Builder model set to Sonnet 5 for every new turn.

Branch-aware by design

A request with no branch targets main — right for a template app. An app built over a repo works on its setup branch, so send/status/stop resolve it and stamp it, or they'd silently hit the wrong line:

sequenceDiagram
    participant CLI as base44 builder send
    participant BE as backend (existing endpoints)
    CLI->>BE: GET /api/apps/{id}/branches
    BE-->>CLI: [setup branch]  — or []  for a template app
    CLI->>BE: POST chat/message?branch_id=…   (none → main)
    BE-->>CLI: turn outcome
Loading

Interactive — base44 code

A full-screen Ink session, composed from the same helper the atoms use — there is one create path, not two. The header mark is rendered, not hand-drawn: a supersampled, aspect-corrected disc in sub-cell block glyphs with a thin slot, octant glyphs on terminals that draw them and quadrant blocks elsewhere (code/logo.ts, pinned by golden tests).

base44 code   (empty dir → first prompt creates the app)
  createAndLinkApp          ← the exact helper `builder new` calls
    createApp | createImportedApp
    writeAppConfig · setAppContext
  resolveActiveBranchId
  runGenesisSession → engine → sendTurn / getFullConversation (apps/api)
$ base44 code
  ● Web app — Base44 template + builder agent
  › a habit tracker with weekly streaks
  ✻ building…                       (live: entities, pages, functions stream in)
  ✓ ready · editor ↗ · preview ↗      (input stays open for the next turn)

$ base44 code --import https://github.com/me/my-store
  ● Repository — github.com/me/my-store

In a linked directory it reopens that app's session; base44 code --app-id <id> opens any app from anywhere, and base44 link attaches a directory to an existing app (the help text and the idle hint name both). The footer chip names the kind of app from its state — web app, game, mobile app, slides — or shows the repository for an app built over one; "import" is never used as a label. In-session: Enter sends, Esc stops the running turn (server-side), arrows/PgUp/PgDn scroll, Ctrl+C/D exit. The mode shows in the header line and a footer chip. /model opens an arrow-navigable model picker (/model <name> switches directly); the current pick shows in the footer.

Dependencies added — flagged for review

For the Ink session: ink, ink-text-input, react (+ @types/react); "jsx": "react-jsx" in tsconfig.json; a small bun plugin in infra/build.ts stubbing Ink's dev-only react-devtools import so the bundle builds; knip.json scans .tsx. bun.lock updated.

Lockfile — flagged for review. bun.lock gains only the entries for these four packages and their tree, recorded in registry-default form ("" URL) like every existing entry; bun also re-nests the older string-width/wrap-ansi family under cliui, yargs, @inquirer/core and msw. Versions and sha512 integrity are unchanged.

Build. infra/bundle.ts owns the runtime externals list and a bundler plugin that stubs Ink's dev-only react-devtools-core import. Standalone binaries now compile through Bun.build({ compile }) instead of spawning bun build --compile, so the same stub applies to them (marking the package external is not enough: the binary hoists the import and exits at startup).

Tests

  • tests/cli/builder.spec.ts (16): template create → settled turn → preview; create over a repository; empty-directory and --path linking; input validation; send branch-scoping + queued; send refuses a code-first project and a Superagent; status, sandbox preview, stop; model list, set, and default; code refuses without a TTY.
  • tests/core/stream.spec.ts (18): the poll-and-diff stream.
  • Full suite green: 83 files / 875 tests. Typecheck + biome clean; bun build OK.

Caveat — the agent's preview-verification tools need the editor open

Not specific to either flow. The agent's screenshot and execute-in-preview tools are answered by the running preview inside the web editor; with no editor tab open they report the preview as unavailable, for both template apps and apps built over a repository. The agent still builds fine from the terminal; only its "look at the running app" step depends on the editor. Building over a repository and picking a model are each enabled per account; the CLI explains the refusal when an account lacks one.


Next steps — not part of this PR

Listed so the line is explicit; none of this ships here.

T2 — CLI

  • Funnel gap — first-prompt classification. Before creating, the web classifies the first prompt and, where the account allows it, offers game mode (with a 2D/3D engine choice) or slides, then creates the app with that type. The CLI skips this and always creates a standard web app, and an app's type never changes after creation — so "flappy bird" from the terminal is built with the web-app stack. Close it by running the same classification in builder new and the code first prompt (honour the verdict by default, --type web|game|slides to pin) and passing the type on create.
  • --clone for apps built over a repository: clone into the linked directory on the setup branch, so git is the pull/push pair. Then builder commit / builder pr (refused on template apps).
  • Blank mode: a repository app from the starter with no repository of your own (needs the backend items below).
  • agents chat <name> "<msg>" and agents conversations under the existing agents group — the runtime endpoint answers synchronously and accepts the platform token. A /agent slash in code decided after.
  • superagent chat | conversations — same shape, separate product; needs its owner and a confirmation story for side effects.
  • Attach and resume: builder open <id> or a picker in code reusing link's app list; show recent history on attach; handle several active branches instead of falling back to main.
  • Plain piped output, claude -p style: when stdout is not a TTY, the reply goes to stdout and progress to stderr (today settled tool lines go to stdout and the reply is not printed); exit codes reflecting the final status (a distinct one for waiting) and --timeout on builder new and builder send.
  • Rollout gate: check a server feature flag from /api/auth/me (feature_flags) at the start of builder new, builder send and code, failing with one line that names it — the same list the web client uses; one new server-side flag.
  • Session input editing: an in-house line editor for Alt+←/→, Alt+Backspace/Ctrl+W, Ctrl+A/E, Ctrl+U/K, replacing ink-text-input (TODO in session.tsx).
  • Creation flags for games and mobile apps (the atoms already work on every builder-driven type); model-list parity (Base 1 behind its flag, NEW badges).

T3 — CLI

  • Live source sync for template apps over the sandbox file commands, with checkpoints — the prerequisite for any create --prompt that brings files onto disk.

Backend

  • Chat refuses code-first (user-managed source) apps, or the editor hides it for them — the CLI guards its side only.
  • A typed github_reauth_required error carrying the reconnect URL (replacing the CLI's string match).
  • Serve the preview-verification bridge per app rather than from one configured backend.
  • Preview verification without the editor open.
  • A canonical starter repo + durable GitHub auth on preview envs — unblocks blank mode.

🤖 Generated with Claude Code

@ayal
ayal force-pushed the terminal-t1 branch 3 times, most recently from 95ff29c to 2678499 Compare September 17, 2026 10:41
@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.1.15-pr.630.04a5262

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.1.15-pr.630.04a5262"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.1.15-pr.630.04a5262"
  }
}

Preview published to npm registry — try new features instantly!

@ayal ayal changed the title feat(cli): base44 app — non-interactive agent build atoms + isolated base44 code session (T1) feat(cli): base44 app — non-interactive agent build atoms + base44 code interactive session (T1) Sep 17, 2026
@ayal ayal changed the title feat(cli): base44 app — non-interactive agent build atoms + base44 code interactive session (T1) feat(cli): build apps with the agent from the terminal — base44 app + base44 code Sep 17, 2026
@ayal
ayal requested a review from netanelgilad September 17, 2026 11:42
@ayal ayal changed the title feat(cli): build apps with the agent from the terminal — base44 app + base44 code feat(cli): build apps with the agent from the terminal — base44 builder + base44 code Sep 17, 2026
…er + base44 code

Non-interactive atoms under `base44 builder`: new (template app, over an
existing GitHub repository, or through the Wix route with a signed instance),
send, status, stop, model. `base44 sandbox preview` prints the preview URL.
Every command supports --json; new and send also --stream-json (one JSON line
per stream event, then a result) and --verbose.

Interactive `base44 code`: an Ink session where the first prompt in an empty
directory creates the app, a linked directory or --app-id opens an existing
one. Live turn rendering with tool results shown Claude-Code style (edits as
diffs, one-line successes, whole errors, Ctrl+O to unfold, a pulsing line for
the running tool), a /model picker, and cards for whatever the agent parks on
the user — approvals, clarifying questions, permissions, secrets with masked
entry, browser steps — answered through the same route the editor uses.

Shared core: create / send / stop / state / branch resolution / conversation
polling with event diffing / pending-input parsing, used by both faces.
Directory rule matches `base44 create`: an empty cwd is the project, else
./<name>; --path picks explicitly. `code` and `send` refuse code-first
projects and Superagents. Credentials (secret values, the Wix signed instance)
never reach telemetry: request bodies are not captured for those calls and
credential-like option values are redacted from the crash context.

Build: the standalone binaries compile through Bun.build so the plugin that
stubs Ink's dev-only devtools import applies to them too. Lockfile gains only
the Ink/React entries, in registry-default form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ayal and others added 5 commits September 19, 2026 10:03
…kinds

A parked OAuth call (connector authorization, GitHub connect) is now handled
the way the editor handles it: start the flow, give the user the link (also
opened in the browser when there is one), poll the connection's status, and
approve the tool call only once it is ACTIVE — approving first made the tool
report "no fresh active connection was stored". Failures and timeouts offer a
retry. New core module: apps/connections.ts (initiate, wait, GitHub status).

Also: select_payment_provider renders as a choice and answers under the
tool's own key; a parked tool this CLI cannot render (waiting_on.kind
input/choice from an unknown tool) shows an "answer in the editor" card
instead of a misleading yes/no; a paywall-ended turn says the workspace is
out of credits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nswer

When the builder parks a tool call, non-interactive callers now learn about
it and can answer without a browser:
- `send`, `new` and `status` return status "waiting" with `pending`: the
  same shape the session's cards use (kind, title, questions and options,
  secret names, permission keys, browser step). `--stream-json` waiting
  events carry it too.
- `send` takes the answer instead of a message: --approve / --reject /
  --skip / --choose <label> (repeat per question, comma for multi-select)
  / --other <text> / --grant <keys> / --secret NAME=env:VAR|file:PATH|-
  / --input <json>; --id picks when several wait. Options and keys are
  validated against what was asked; secret values are never accepted as
  plain text.
- policy for scripts: --auto-approve (approval-kind pauses, all permissions)
  and --skip-questions (the builder decides). Never secrets, browser steps
  or unknown forms — those always come back as waiting.
- a message sent while a call is parked is refused with the pending list
  instead of a bare 4xx.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…OAuth step

The card offered y/n and sent an empty approval, so the tool answered "No
credentials were provided". Match the editor: a name, then Base44's
credentials (b) or your own client id and secret (y), secret typed hidden.
Non-interactive: --connector-name [--client-id --client-secret env:VAR].

Also stop re-announcing a "needs your input" line for a call already
answered in this session — the backend keeps it waiting until the resumed
turn persists, and the transcript showed the pause twice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arrow terminals

request_oauth_authorization verifies the answer by the connection_id in
extra_user_input — the web threads it through, and an empty approval reads
as "no fresh active connection was stored" even after a successful OAuth.
The card now remembers the id the initiate call returned and approves
with it.

The footer (chip · dir · editor · preview) was one truncate-end row, so a
narrow terminal cut the editor and preview links off. It wraps onto as many
rows as it needs and the layout accounts for them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
chat/message and submit-tool-call-input stay open for the whole turn, and
the edge or a proxy can cut the socket minutes after the backend took the
request — surfacing as a bare "fetch failed" / ECONNRESET with no status.
The session reported that as "answer failed" while the resumed turn was
visibly streaming. One classifier now covers 5xx, timeouts and transport
drops for both submit and answer; only a real API rejection is reported.
The non-interactive send recovers the same way: on a drop it waits for the
turn to settle from the conversation and returns that outcome.

Co-Authored-By: Claude Fable 5.1 <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