framework
Version:
The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.
170 lines • 9.71 kB
TypeScript
import { type GitRunner } from '../project.js';
import type { Driver, DriverPromptOptions, DriverSession, DriverStartOptions, DriverTurn } from './types.js';
/**
* A {@link Driver} that hands the task to **Claude Code on the web** (#610): it starts a
* real cloud session on claude.ai and returns its id and URL.
*
* The mechanism is the CLI's own `--cloud` flag, so the account, the auth and the
* quota are the user's, exactly as with the local driver (#495). Nothing here drives
* the claude.ai UI: no browser, no extension, no scraping — the two earlier candidates
* for this issue, both of which the Usage Policy rules out.
*
* **Why a pty.** `--cloud` refuses to run when stdout is a pipe, because a non-interactive
* invocation would silently run locally instead. That check is about the *terminal*, not
* about a human, so running the CLI under a pty satisfies it. `script` supplies the pty
* (present on macOS and Linux) and the prompt travels in the environment, never inside a
* shell string — the command string is a fixed literal, so no prompt text can reach the
* shell as syntax.
*
* **What this target is, and is not.** It is a hand-off: the session runs on Anthropic's
* infrastructure, does its own git worktree and opens its own PR, at 0% local CPU — the
* whole point of #610. What it is not is a streamed peer like the local, device and
* Actions targets, and that is not a shortcut in this implementation but a property of
* the surface: a cloud session exposes **no read-back API** of any kind — no status, no
* transcript, no output endpoint, only the session URL. So the turn resolves once the
* session is created, and following the work happens on claude.ai, or by pulling it back
* with `claude --teleport <id>`.
*
* For the same reason there is no `readCode`: the workspace lives in a cloud VM this
* machine never sees.
*/
export declare class CloudDriver implements Driver {
private readonly opts;
readonly id = "claude-web";
constructor(opts?: CloudDriverOptions);
start(opts: DriverStartOptions): Promise<DriverSession>;
}
/** Options for {@link CloudDriver}. */
export interface CloudDriverOptions {
/** Claude Code binary. Default `"claude"`. */
bin?: string;
/** Give up on session creation after this long, in ms. Default 120000. */
timeoutMs?: number;
/** Run one pty-hosted invocation. Injected in tests; defaults to a real `script` pty. */
runPty?: RunPty;
/** Runs git for the pre-hand-off push (#1320). Injected in tests; defaults to real git. */
git?: GitRunner;
/** The CLI's config file the pre-hand-off trust write (#1493) touches. Injected in tests; defaults to `~/.claude.json`. */
claudeConfig?: string;
/**
* Unique tag mixed into the session id. Default a random token. Injected in tests for a
* stable id, and load-bearing in production for the same reason it is in the Actions
* driver: a fresh `framework run` process restarts the counter, so without it every
* run's first session would carry the same id.
*/
agentTag?: () => string;
}
/** One pty-hosted invocation: stream its output, resolve when it ends. */
export type RunPty = (opts: AgentPtyOptions) => Promise<void>;
/** What {@link RunPty} needs to run one invocation. */
export interface AgentPtyOptions {
/** Claude Code binary to run under the pty. */
bin: string;
/** The prompt, handed over through the environment rather than the command line. */
prompt: string;
/** Model id to pass through, when one was chosen. */
model?: string | undefined;
/**
* The ref the session clones at (#1320), pushed to origin just before this invocation.
* Absent when that push failed — the CLI then pins its own default, which is the current
* local branch and fails in-session when that branch is not on origin.
*/
ref?: string | undefined;
/** Workspace the CLI runs in — the repo whose remote the cloud session clones. */
cwd: string;
/** Called with each chunk of terminal output. */
onData: (chunk: string) => void;
/** Stop the invocation: the caller has what it needs, or the agent was aborted. */
signal: AbortSignal;
}
/**
* The project root an agent worktree belongs to, which is where trust has to be granted.
*
* The CLI records trust per directory and everything under a trusted directory inherits it
* (verified live: a fresh worktree of a trusted root boots straight to the REPL, a fresh
* worktree of an untrusted root always shows the dialog). An agent's cwd is an ephemeral
* worktree — gone before the user could act on advice that names it — so the only advice
* that works is: trust the root once, and every agent worktree under it is covered.
*/
export declare function trustRootOf(cwd: string): string;
/**
* The rule between the task and the injected instructions in a hand-off prompt (#1497).
* Exported so a test can pin the exact seam the claude.ai reader sees.
*/
export declare const CLOUD_PROMPT_SEPARATOR = "===============================";
/**
* Assemble the one prompt a cloud session receives (#1497). Unlike every streamed driver —
* where the system channel is invisible plumbing — this whole string is what a *human* reads
* when they open the claude.ai session. So the task comes first (it is what the user is
* looking for), and each injected block follows behind a hard `===` rule with a one-line
* label, because the blocks' own markdown headers run into each other and read as one
* confusing document without it.
*/
export declare function cloudHandOffPrompt(task: string, ...injected: (string | undefined)[]): string;
/**
* One hand-off to Claude Code on the web — **exactly one, for the life of the session.**
*
* An agent is not a single prompt. The loop prompts again for every pass (plan, build, review,
* the TODO backlog), so a driver that started a cloud session per prompt turned one agent into
* six of them on the account. That is not a caveat, it is the wrong shape: the same task
* handed to six independent cloud VMs is six agents racing on one repo.
*
* So the first prompt hands off, and every later one reports the hand-off that already
* happened without spending another session. There is no continuation to offer either way —
* the CLI can start a cloud session and pull one back, but it cannot send a second message
* to one, so the honest answer to "keep going" is "this agent is already over there".
*/
export declare class CloudSession implements DriverSession {
private readonly config;
private readonly startOpts;
readonly id: string;
readonly cwd: string;
private readonly emit;
private readonly framing;
private readonly controllers;
private disposed;
/** The cloud session this agent was handed to, once it exists. Set at most once. */
private handedOff;
/** The hand-off anchor commit (#1601), once it is on origin. Set at most once, with the hand-off. */
private anchorSha;
constructor(config: CloudDriverOptions, startOpts: DriverStartOptions);
prompt(text: string, opts?: DriverPromptOptions): Promise<DriverTurn>;
/**
* Report where this agent went. The `first` hand-off emits the `cloud <url>` action the agent
* view links through to — mirroring the Actions driver's `run <url>` — and a later pass
* says the work is already there, so a loop that keeps prompting cannot read the same turn
* as fresh progress and cannot spend a second session.
*/
private report;
dispose(): Promise<void>;
}
/**
* The shell command `script` hosts. A **fixed literal**: the prompt and the model arrive
* as environment variables, so nothing the user typed is ever parsed as shell syntax.
* `${FW_CLOUD_MODEL:+...}` adds the model flag only when one was chosen.
*
* **The prompt has to come directly after `--cloud`.** The description is that flag's own
* value rather than a loose positional argument, so anything in between claims the slot and
* the CLI stops with "--cloud requires a description". That is why this failed on an account
* with a model preference and worked without one: the model flag was sitting in the slot.
* Exported so a test can pin the order, which is load-bearing and not otherwise observable.
*
* `--ref` names the origin ref the session clones at (#1320) — undocumented in the CLI's
* help but real, and the only revision spelling that works: the CLI's default pin is the
* current local branch, which the cloud side cannot resolve when it is not on origin, and a
* slash-carrying name (every `the-framework/...` branch) never resolves at all
* (anthropics/claude-code#87235). The ref is pushed just before this runs; see the pre-push
* in {@link CloudSession.prompt}.
*/
export declare const CLOUD_COMMAND = "exec \"$FW_CLOUD_BIN\" --cloud \"$FW_CLOUD_PROMPT\" ${FW_CLOUD_MODEL:+--model \"$FW_CLOUD_MODEL\"} ${FW_CLOUD_REF:+--ref \"$FW_CLOUD_REF\"}";
/**
* Extra environment for the CLI invocation (#1320): with statsig disabled its gates read
* false, which turns off the server-side `tengu_ccr_bundle_seed_enabled` experiment — the
* flag that converts a failed GitHub-App preflight into a silent local-bundle upload with no
* remote and no push access (anthropics/claude-code#81776). With the gate off, the same
* failed preflight falls through to a repo-bound session that clones from GitHub and can
* push, which is the entire point of handing work to the cloud. Drop this once the upstream
* preflight accepts connected-account access.
*/
export declare const CLOUD_ENV: Readonly<Record<string, string>>;
//# sourceMappingURL=cloud.d.ts.map