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.
299 lines • 17.7 kB
TypeScript
import { type AgentLocation } from './agent-location.js';
import { type HandoffLevel } from './handoff-level.js';
/**
* The multi-project registry (#390): the list of projects the user has
* installed The Framework into, kept as a single JSON file `.bashrc`-style —
* `$HOME/.the-framework.json` — so it is the user's responsibility to re-create
* per machine. The same file also holds the user's dashboard preferences (#410),
* so the daemon owns one user file and the UI never needs localStorage.
*/
/** One registered project. */
export interface ProjectRecord {
/** Stable, URL-safe id derived from the path. */
id: string;
/** Absolute repo path. */
path: string;
/** ISO timestamp the project was added. */
addedAt: string;
}
/**
* The dashboard's Global options (#410), persisted next to the project list so they
* survive restarts without localStorage — the daemon reads/writes them, the SPA reads
* them over `POST /_rpc/onPreferences`. Mostly flat booleans mirroring the Start form's
* toggles; every field is optional and absent means off, except where a field documents its
* own default below.
*/
/**
* A user-defined preset (#626): a named prompt the user saved to re-run their own high-signal
* prompts, sitting beside the built-in presets in the Start form. Just data — the label is the
* button, the prompt is loaded verbatim into the editor and run as a `prompt` kind (unlike the
* built-ins, whose text is a compiled render function). `id` is stable so edits/deletes address one.
*/
export interface CustomPreset {
id: string;
label: string;
prompt: string;
}
export interface Preferences {
vanilla?: boolean;
/** On-before-mergeable prompt (#326): on setReadyForMerge(), queue the quality follow-ups as TODO entries. */
onBeforeMergeableQuality?: boolean;
/** Give the agent a real browser via chrome-devtools-mcp during the agent (#452); maps to `--browser`. */
browser?: boolean;
/**
* How far a finished session publishes itself (#1102/#1216/B5): keep it local, push the branch,
* open a draft PR, or merge that PR. Absent = {@link DEFAULT_HANDOFF} (`pr`).
*
* Default-on, unlike most of this file, because it is what makes the handoff zero-config: the
* old behaviour was a button nobody was obliged to press, and work that stayed on a local
* branch nobody was told about (#860). A session can still opt out from its action bar.
*/
handoff?: HandoffLevel;
/**
* Transparent mode (#625): run the wrapped agent raw — no framework system prompt, emit
* protocols, consumption guard, dashboard, or TODO loop, so an agent is identical to `claude -p`.
* The coarse master off-switch ("only pick what you need"); maps to `--transparent`. Absent = off.
*/
transparent?: boolean;
/** Fire a browser notification when a new item lands on the "needs you" queue (#627). Absent = on. */
notifyBrowser?: boolean;
/**
* Also notify on plain agent activity — an agent started, an agent finished (#627). The default-off
* counterpart to the always-on "needs you" notifications: it keeps you loosely informed of the
* pipeline moving even when nothing needs you. A *category* toggle: it composes with the method
* toggles ({@link notifyBrowser} / {@link notifyDiscord}), so activity reaches whichever are on.
*/
notifyNewActivity?: boolean;
/**
* The "needs you" category (#627): notify when an agent is awaiting your answer or a PR is ready
* to review. A *category* toggle, like {@link notifyNewActivity}, composing with the method
* toggles ({@link notifyBrowser} / {@link notifyDiscord}). **Absent = on**: unlike the other
* flat opt-in booleans, human-intervention pings are the baseline The Framework leans on, so an
* unset preference keeps them firing; a user turns them off explicitly.
*/
notifyHumanIntervention?: boolean;
/** The model to run on (#628), e.g. `opus` / `sonnet`; maps to an agent's `--model`. Absent = the driver's default. */
model?: string;
/** Which coding agent drives the agent (#650): `claude` or `codex`; maps to `--agent`. Absent = the default (`claude`). */
driver?: string;
/** Preferred editor for "Open in editor" (#727): an editor CLI (e.g. `code`, `cursor`, `zed`).
* Absent falls back to `$FRAMEWORK_EDITOR`, then `code`. */
editor?: string;
/** Dashboard color theme (#725): `system` (follow the OS, the default), `light`, or `dark`. Absent = system. */
theme?: 'system' | 'light' | 'dark';
/** Where a run executes (#1050/#610): `local` (this device, the default), `actions` (a fresh GitHub Actions runner) or `web` (a Claude Code cloud session); maps to `--run-on`. Absent = local. */
target?: AgentLocation;
/**
* Post a Discord message when a new item lands on the "needs you" queue (#627). Absent = off:
* unlike the in-browser toggle, Discord reaches you when no dashboard is open, so it is opt-in.
* Gates the daemon watcher *on top of* a `DISCORD_WEBHOOK` being set (the webhook is where to
* post; this is whether to).
*/
notifyDiscord?: boolean;
/**
* Auto PM (#685): let the daemon start a PM agent by itself when the agent queue has run dry
* and there is plenty of budget left, so leftover subscription quota goes on the roadmap
* instead of expiring. **Absent = off**: it spends the user's allowance without being asked,
* so it is opt-in like {@link notifyDiscord} rather than a baseline.
*/
autoPm?: boolean;
/**
* The browser bridge (#1237): let an extension running in the user's own Claude session report
* the question a Claude web agent is parked on, so it shows in the dashboard rather than only on
* claude.ai. **Absent = off.** It opens the daemon's one route reachable from another origin,
* so it is opt-in rather than a baseline, and turning it on is what mints the bridge token.
*/
bridge?: boolean;
/**
* The routines {@link autoPm} must not fire, by {@link AutoPmJob.name} (#1209). Absent or empty
* = every routine runs, which is what the sweep did before this existed.
*
* Opted *out* rather than opted in, so the list only ever names exceptions: a routine added in a
* later version is on for everyone, instead of silently never running for whoever saved the
* setting before it shipped. It names routines rather than indexing them for the same reason
* {@link AutoPmJob.drains} is a flag — a reorder must not move which one is switched off.
*/
autoPmOptOut?: string[];
/**
* How many agents the routine may keep going at once on one project (#1204). Absent defaults to
* `DEFAULT_AUTO_PM_CONCURRENCY`, and the value is clamped to `MAX_AUTO_PM_CONCURRENCY`.
*
* Only the draining routine fans out: it takes work *off* the queue, one pinned entry per agent,
* so several at once do disjoint work. The rotation invents work and each of its jobs rewrites
* the queue file, so it stays one agent per tick whatever this says.
*/
autoPmConcurrency?: number;
/**
* How far the automatic-consumption limit sits from the quota boundary, in percentage points
* (#960). Absent defaults to {@link DEFAULT_SPEND_OFFSET} — a half-day cushion ahead of the
* boundary — rather than sitting exactly on it (#960 Edit).
*
* Negative holds unattended work back further; positive lets it borrow into the days still to
* come. It is an *offset* rather than an absolute percentage so the limit travels with the
* boundary as the week goes on, instead of being overtaken by it on day two.
*/
autoSpendOffset?: number;
/** User-defined presets (#626): the user's own saved prompts, shown beside the built-in presets. */
customPresets?: CustomPreset[];
/**
* Whether the Overview's Onboarding checklist has been dismissed (#958). Absent = show it,
* so a fresh install is walked through setup; dismissing only hides it on the Overview, and
* the same checklist stays available on the settings page.
*/
onboardingDismissed?: boolean;
}
export { MAX_SPEND_OFFSET, DEFAULT_SPEND_OFFSET, DEFAULT_AUTO_PM_CONCURRENCY, MAX_AUTO_PM_CONCURRENCY, } from './preference-defaults.js';
/**
* The credentials the daemon needs to reach a third party, set from the dashboard (#1095).
*
* Their tier is the {@link Registry.daemonToken} one, not {@link Preferences}: top-level, so
* neither the browser bundle nor the per-project override map can ever carry them. Nothing
* reads a value back out to a client — the dashboard is told only that one is *present*
* ({@link DiscordCredentialStatus}) — so the registry file stays the one place they exist.
*
* The alternative was a second file. This one already holds `daemonToken`, which authenticates
* every request to a network-reachable daemon, so the file is a secret store since #1051; a
* second one would only spread the same exposure over two paths to keep 0600 on.
*/
export interface RegistrySecrets {
/** Where Discord notifications are posted (#627). Overridden by `DISCORD_WEBHOOK` when that is set. */
discordWebhook?: string;
}
/** The persisted registry file shape (#410): the project list plus the user preferences. */
export interface Registry {
projects: ProjectRecord[];
preferences: Preferences;
/**
* The shared daemon token (#1051): generated on the first non-loopback bind and reused after.
* A top-level field, deliberately not a {@link Preferences} one, so it is never shipped to the
* browser bundle. Absent on a loopback-only machine.
*/
daemonToken?: string;
/** Third-party credentials set from the dashboard (#1095). Absent until one is saved. */
secrets?: RegistrySecrets;
}
/** A read/write handle for the user preferences, wired into the dashboard's context by the daemon. */
export interface PreferencesStore {
read(): Promise<Preferences>;
save(preferences: Preferences): Promise<void>;
/**
* Merge only the keys the caller changed (#1148) and hand back the stored result. Preferred over
* {@link save}, which replaces the whole block from a snapshot that may already be stale.
*/
patch(patch: Preferences): Promise<Preferences>;
}
/** The registry file name: a single file under `$XDG_CONFIG_HOME` (dotted under `$HOME`). */
export declare const REGISTRY_FILE = "the-framework.json";
/** Owner read/write only: the file holds the daemon token (#1051) and the Discord credentials (#1095). */
export declare const REGISTRY_FILE_MODE = 384;
/**
* Deterministic, URL-safe id for a project path: the sanitized basename plus a
* short hash of the full path, so two repos named alike still get distinct ids.
* Pure; same path always yields the same id.
*/
export declare function projectId(path: string): string;
/**
* The registry file path, resolved from `env` (injectable so tests never touch
* the real home): `$XDG_CONFIG_HOME/the-framework.json` when set, else the
* dotted `$HOME/.the-framework.json`. A single file, not a directory (#390).
*/
export declare function registryPath(env: NodeJS.ProcessEnv): string;
/** Minimal fs seam so the registry is unit-testable without touching disk. */
export interface RegistryFs {
/** Rejects when the file is absent. */
read(path: string): Promise<string>;
write(path: string, contents: string): Promise<void>;
/** Recursive; used on the registry file's parent dir. */
mkdir(path: string): Promise<void>;
/**
* Replace `to` with `from`, atomically. Optional only so an existing implementation of this
* seam keeps compiling; without it {@link writeRegistry} falls back to the truncate-then-write
* this method exists to avoid (#991).
*/
rename?(from: string, to: string): Promise<void>;
/**
* Narrow a file's permissions. Optional, and best-effort at the call site: this file holds the
* daemon token (#1051) and the Discord credentials (#1095), so it is written owner-only — but a
* filesystem that cannot express that (Windows, a FAT volume) must not fail the write.
*/
chmod?(path: string, mode: number): Promise<void>;
}
/** A {@link RegistryFs} backed by `node:fs/promises`. See {@link nodeFs}. */
export declare function nodeRegistryFs(): RegistryFs;
/**
* Keep only well-formed custom presets (#626): each needs a non-empty id, label, and prompt;
* label/prompt are trimmed and length-capped, the list capped at {@link CUSTOM_PRESET_LIMITS.count},
* and duplicate ids dropped. A malformed entry is skipped, not thrown — a bad registry never breaks the read.
*/
export declare function sanitizeCustomPresets(value: unknown): CustomPreset[];
/**
* Read the whole registry. Forgiving: a missing / unreadable / malformed file — or one in a shape
* this no longer writes — yields an empty registry, never throws. Projects are deduped by resolved
* path and unknown preference fields are dropped.
*/
export declare function readRegistry(fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<Registry>;
/**
* Read the registry's project list. Forgiving: a missing / unreadable / malformed
* file yields `[]`, never throws. Deduped by resolved path, first wins.
*/
export declare function listProjects(fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<ProjectRecord[]>;
/**
* Register a project. Idempotent by resolved path: when the path is already
* registered, the existing record is returned untouched (addedAt survives);
* otherwise the new record is appended and the file written back (preferences preserved).
*/
export declare function addProject(path: string, addedAt: string, fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<ProjectRecord>;
/** The user's dashboard preferences (#410), or `{}` when none are stored. */
export declare function readPreferences(fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<Preferences>;
/** Persist the dashboard preferences (#410), sanitized, preserving the project list. */
export declare function writePreferences(preferences: Preferences, fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<void>;
/**
* Merge `patch` over the stored preferences (#1148) and return the result.
*
* The counterpart to {@link writePreferences}, which replaces the whole block: a client that
* sends its entire snapshot replays every value it happens to hold, so a dashboard tab opened
* before someone else's change silently reverted it on the tab's next write, whatever key that
* write was actually about. Sending only the changed keys makes a write touch only what it names.
*
* Clearing needs no sentinel: {@link sanitizePreferences} already drops blank strings and empty
* lists, so `{ editor: '' }` merges in as blank and comes out absent, which is how the dashboard
* clears the editor today.
*/
export declare function patchPreferences(patch: Preferences, fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<Preferences>;
/**
* The shared daemon token (#1051): read the persisted one, or generate + persist it now. Called
* only on a non-loopback bind, so a loopback-only machine never grows one. Serialized with the
* other mutators so two concurrent binds can't each write a different token. `base64url` of 32
* random bytes: URL-safe, so it drops straight into a `?token=` without encoding.
*/
export declare function ensureDaemonToken(fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<string>;
/**
* The stored third-party credentials (#1095), or `{}` when none are set. Daemon-side only —
* every caller is a service that needs the value itself, never a client read: what the dashboard
* gets told is presence, in {@link RegistrySecrets}'s doc sense.
*/
export declare function readSecrets(fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<RegistrySecrets>;
/**
* Merge a patch into the stored credentials (#1095), leaving everything else in the file alone.
*
* A patch, not a whole-object write, because the caller is a UI that edits one field: the bot
* dialog must not clear the webhook by not knowing it. An explicit `null` (or a blank string)
* clears a key — that is the Clear button — while `undefined` leaves it as it was, so "not
* mentioned" and "removed" stay different things. Serialized with the other mutators.
*/
export declare function writeSecrets(patch: Partial<Record<keyof RegistrySecrets, string | null>>, fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<void>;
/** The persisted daemon token (#1051), or `undefined` when none exists. A pure read, so a process
* that only prints the reachable URL never generates one. */
export declare function readDaemonToken(fs?: RegistryFs, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
/** A {@link PreferencesStore} bound to the real registry file, wired by the daemon so the
* dashboard's preferences RPCs read/write the user's home file.
*
* `onChange` is handed **the keys the caller wrote**, not the merged result, so a listener can
* tell "this write switched the setting on" from "it was already on and something else changed"
* (#1161). It runs after the write has landed, and its failure is swallowed: the save succeeded,
* and a listener must not be able to report otherwise. Same shape as the Discord store's
* `onChange` (#1095), for the same reason — a setting saved in the browser has to reach the
* daemon's own services without a restart.
*/
export declare function registryPreferencesStore(fs?: RegistryFs, env?: NodeJS.ProcessEnv, onChange?: (written: Preferences) => void): PreferencesStore;
//# sourceMappingURL=registry.d.ts.map