UNPKG

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
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