@volter/world 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ # volter-world — operating a World
2
+
3
+ A World contains only the vendors deliberately selected for substitution in the current workflow.
4
+ Review detected vendors before including them. The app uses its real SDKs unchanged;
5
+ environment injection, a Node preload and a scoped HTTPS proxy route configured vendors to twins.
6
+
7
+ The state model is **Neon for every SaaS**: a log, checkpoints and branches over a parent position.
8
+ Git-inspired verbs describe operations; there is no staging or commit step. Push moves entries
9
+ between Worlds. Deploy performs them against a vendor at a real-system root under its policy.
10
+ The canonical [model](https://github.com/volter-ai/twin/blob/main/docs/concepts/the-model.md),
11
+ [lifecycle](https://github.com/volter-ai/twin/blob/main/docs/concepts/worlds.md) and
12
+ [CLI reference](https://github.com/volter-ai/twin/blob/main/docs/reference/cli.md) own the details.
13
+
14
+ ## Set up a World for an app
15
+
16
+ In the app's folder (the [guide](https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md)):
17
+
18
+ ```bash
19
+ npm install -g @volter/world # once; `volter agents install` then adds this skill and the MCP server to your agents
20
+ volter world init # detects the vendors from the app's manifests and env names; writes .volter/world.json
21
+ volter world up
22
+ volter world run -- npm test # the app's own test command
23
+ volter world log # what the app did at each vendor
24
+ ```
25
+
26
+ Show the person the vendors `init` detected, and why, before keeping them. Keys the app reads
27
+ (named in `.env.example`) get throwaway values in the World's env; never put real credentials in
28
+ a World to get a test past validation. `volter world view --no-open` runs the World with its
29
+ dashboard and prints its link for the person; it keeps running, so start it in the background,
30
+ and stop a World you started with `up` first (`down` keeps its state).
31
+
32
+ ## Share it on a platform
33
+
34
+ A team shares Worlds on a Volter World platform (Volter's cloud, or one the team runs):
35
+
36
+ ```bash
37
+ volter login <platform url> # prints a link and a code: the PERSON approves it in their browser
38
+ volter remote add origin <org>/<world> --create # links this World to that one, making it from world.json's vendors
39
+ volter world changeset -m "<what and why>" # the unpushed changes, as one reviewable changeset
40
+ volter world push # sends it; the platform answers with receipts
41
+ ```
42
+
43
+ You cannot approve a sign-in; hand the link and code to the person and wait. Ask the person for
44
+ `<org>/<world>` rather than guessing it. Keys for apps and CI are made on the platform.
45
+
46
+ ## The MCP tools
47
+
48
+ `volter mcp` serves the same verbs to an agent: `world_init`, `world_up`, `world_run`,
49
+ `world_status`, `world_log`, `world_diff`, `world_view`, `world_branch`, `world_checkout`,
50
+ `world_reset`, `world_down`, `platform_login`, `platform_whoami`, `remote_link`, `world_changeset` and
51
+ `world_push`,
52
+ each taking the app's folder as `dir`. They run the `volter` command itself, so their answers and
53
+ refusals are the command's: act on a refusal's reason rather than working around it.
54
+
55
+ ## Run through the World
56
+
57
+ Run apps and tests inside a World, never bare. For a repo with `.volter/world.json`:
58
+
59
+ ```bash
60
+ volter world up
61
+ volter world run -- npm test
62
+ volter world down
63
+ ```
64
+
65
+ `volter world up` resumes a stopped branch's state and seeds a fresh branch.
66
+ `volter world run` runs a command inside that World; it does not own teardown.
67
+ `reset` discards the current branch's state and reloads default data. `down` retains state;
68
+ `down --purge` removes it after verified teardown, unless another local branch references it.
69
+ Fresh boot, reset and prune also refuse to remove a referenced parent; stop compute with
70
+ ordinary `down`, or remove dependent branches first.
71
+
72
+ For existing operator configs, use the explicit name and root:
73
+
74
+ ```bash
75
+ volter-world up <config> --root <twins-checkout> --env-file <live-env> --owner <task>
76
+ # From the app repo so the seed's SDK dependencies resolve:
77
+ volter-world attach <world> --root <twins-checkout> -- bun <seed-entry>
78
+ volter-world attach <world> --root <twins-checkout> -- <command...>
79
+ volter-world down <world> --root <twins-checkout>
80
+ ```
81
+
82
+ The lower-level `volter-world up` starts a fresh instance: reseed after each successful boot.
83
+ Do not confuse this with the resumable `volter world up` API. The env file passed to `up` is the
84
+ live env; an init-generated preview is not. Use `url` and `app-url` to discover endpoints rather
85
+ than parsing env files. Registered commands carry `VOLTER_WORLD`.
86
+
87
+ For a task that owns the entire lifecycle, use:
88
+
89
+ ```bash
90
+ volter-world run <config> --root <twins-checkout> --env-file <live-env> --owner <task> -- <command...>
91
+ ```
92
+
93
+ This operator command owns boot, its command and teardown. `--keep` makes it persistent.
94
+ Its cleanup process handles caller loss if it survives; uncertain teardown retains diagnostics
95
+ and lifecycle evidence. Stop registered consumers before explicitly bringing their World down.
96
+
97
+ ## State and behavior
98
+
99
+ Create stored data through the vendor's own API using the real SDK pointed at the twin.
100
+ Identity comes from credentials in the vendor's manner; inspect `GET /twin` for the twin's
101
+ identity and time rules. Use `clock set` and `clock advance` for backdated history. Without a
102
+ frozen World clock, the runtime uses wall time.
103
+
104
+ Author judgment, stateless lookups and faults in `handlers/<vendor>.json` in the World config
105
+ directory (typically `.volter/handlers/`). These are ordered, first-match scenario rules.
106
+ Edit the file and restart to load changes; a handler must never fake success for a stored
107
+ mutation. `GET /twin/scenario` shows handler matches and misses; `tail` records misses to author.
108
+ Model twins serve deterministic scripts or labeled stubs, so verify calls and state changes,
109
+ not the quality of prose from an unconfigured model.
110
+
111
+ Local default data is synthetic. A World cloned or refreshed from a real root may contain real
112
+ records; treat its files accordingly. Fake opaque keys are intentional. If an SDK parses a key
113
+ locally, use `fake-env` to produce a structurally valid throwaway credential; never substitute
114
+ real credentials to get a local test past validation.
115
+
116
+ ## Capacity and ownership
117
+
118
+ Use `resources` to inspect actual filesystem capacity and registered lifecycle owners/consumers.
119
+ Runtime v3 does not reserve speculative per-World memory or storage. Configure supported limits
120
+ in the execution backend; deprecated `resources` declarations are not enforced limits.
121
+ On a storage failure, inspect the actual destination and backend error. Preview `prune` for
122
+ eligible stopped instance files; it never stops a running World. Inspect ownership before apply.
123
+ Unknown use remains unknown; age never proves safety. Preserve cleanup evidence and parent
124
+ history. Never delete another actor's World, worktree, package cache or rate-budget ledger.
125
+ Migrate an old installation only after verified teardown with its pinned runtime, then pin all
126
+ entrypoints to the new version and resume retained state. Never run two runtime versions over
127
+ one instance or erase legacy ownership records to make startup pass.
128
+
129
+ Worlds own their declared infrastructure. Start, inspect and stop it through World commands;
130
+ never operate its underlying process, container or database separately. Install locked repository
131
+ dependencies from the normal registry/cache; use a registry twin only when registry behavior
132
+ needs substitution. Build workspace packages when needed. Do not delete `node_modules` as a World repair.
133
+
134
+ When booting an app, register its URL as the last step with `app-url --set <url>` (or `--detect`).
135
+ An app declared as a World service supplies its own endpoint record. Consumers read `app-url`.
136
+ An app that answers as its own production hostnames (`app.example.com`) records them with
137
+ `app-url --host <name>`: inside the World they reach the app with their Host, `x-forwarded-host` and
138
+ `x-forwarded-proto` (https when the caller used TLS).
139
+
140
+ ## Diagnose before changing the app
141
+
142
+ 1. Check whether the failing vendor belongs in this World before seeding or repairing its twin.
143
+ For app/test execution, check `VOLTER_WORLD` and the intended World environment.
144
+ 2. Run `doctor`, then `tail --no-follow`, and inspect the twin's `GET /twin` manifest.
145
+ 3. Run `covers <world> --repo <app>` for missing vendor coverage.
146
+ 4. Determine whether the failure belongs to the app, twin fidelity, scenario, routing or runtime.
147
+
148
+ Sandbox mode refuses untwinned destinations in cooperating clients, including Node sockets a
149
+ client opens around the injector. It does not constrain binaries or non-Node processes that
150
+ ignore the injector/proxy; network isolation needs an enforced boundary.
151
+ Do not disable routing to hide a coverage gap.
152
+
153
+ Current packages are `@volter/world`, `@volter/world-core`, `@volter/world-runtime`,
154
+ `@volter/world-access`, `@volter/world-attach` and `@volter/twin-<vendor>`. Use the CLI reference for commands instead of
155
+ retired `volter-twin`, `plan` or `review` examples.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: volter-world
3
+ description: Set up, run and diagnose an app inside a Volter World (twins of Stripe, Slack, GitHub, OpenAI and the other SaaS APIs it calls), share it on a Volter World platform, with explicit ownership and cleanup. Use when asked to set up Volter World, run an app or its tests against twins, look at what an app did at a vendor, branch or reset that state, or link a World to a team's platform.
4
+ ---
5
+
6
+ # Operating a World
7
+
8
+ Read [AGENTS.md](./AGENTS.md) before setting up, running or changing a World. It is the single
9
+ operator instruction source shipped with this skill: setting a World up for an app, linking it
10
+ to a platform, the `volter mcp` tools, and the lifecycle distinction between the product command
11
+ `volter world` and the explicit operator command `volter-world`.
12
+
13
+ The [documentation map](https://github.com/volter-ai/twin/blob/main/docs/README.md) links the
14
+ canonical model, task guides and command reference. Do not maintain a second copy here.
@@ -0,0 +1,10 @@
1
+ export type AgentPromptOptions = {
2
+ /** A platform to share the World on: its address. */
3
+ platform?: string;
4
+ /** The World on that platform, `<org>/<world>`; without it, the agent asks. */
5
+ world?: string;
6
+ /** Phrase the steps for an agent that has `volter mcp`'s tools rather than a shell. */
7
+ tools?: boolean;
8
+ };
9
+ /** The prompt, as plain text a person copies. */
10
+ export declare function agentPrompt(o?: AgentPromptOptions): string;
@@ -0,0 +1,23 @@
1
+ // The prompt a person hands their coding agent to set Volter World up for their app (docs/contributing/architecture.md,
2
+ // "The way in is the app's folder"): one text for every place it is offered (the landing page, the platform's Get
3
+ // started, the dashboard's Connect, `volter mcp`'s get-started prompt). Pure, with no imports: browser bundles use it.
4
+ /** The prompt, as plain text a person copies. */
5
+ export function agentPrompt(o = {}) {
6
+ const cmd = (shell, tool) => (o.tools ? `${tool} (\`${shell}\`)` : `\`${shell}\``);
7
+ const steps = [
8
+ 'Set up Volter World for this app, so its calls to Stripe, Slack, GitHub, OpenAI and the other services it uses go to local twins instead of the real APIs. The app keeps its real SDKs and code.',
9
+ '',
10
+ o.tools
11
+ ? '1. You have the Volter World tools (volter mcp). If the `volter` command is missing, install it: `npm install -g @volter/world`.'
12
+ : '1. Install the command and your agent tooling: `npm install -g @volter/world`, then `volter agents install --yes`.',
13
+ `2. In the app's folder, ${cmd('volter world init --install', 'world_init')}: it detects the vendors the app calls and installs their twins. Show me the vendors it detected and why, before keeping them.`,
14
+ `3. ${o.tools ? 'Start it with world_up' : 'Start it: `volter world up`'}, then run the tests inside it: ${cmd('volter world run -- <the test command>', 'world_run')}. Fix anything that fails because a call went to the real service.`,
15
+ `4. Tell me what the app did at each vendor (${cmd('volter world log', 'world_log')}), and give me the dashboard link (${o.tools ? 'world_view' : '`volter world view --no-open`, started in the background: it keeps running'}).`,
16
+ ];
17
+ if (o.platform) {
18
+ const target = o.world ?? '<org>/<world>';
19
+ steps.push(`5. Share it with my team on ${o.platform}: ${cmd(`volter login ${o.platform}`, 'platform_login')} gives a link and a code for me to approve in my browser. Once I have, link it with ${cmd(`volter remote add origin ${target} --create`, 'remote_link with create: true')}${o.world ? '' : ' (ask me for <org>/<world>)'}, then send what the app did: ${cmd('volter world changeset -m "<what and why>"', 'world_changeset')} and ${cmd('volter world push', 'world_push')}.`);
20
+ }
21
+ steps.push('', 'The guide: https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md');
22
+ return steps.join('\n');
23
+ }
@@ -0,0 +1,15 @@
1
+ export type Agent = 'claude' | 'cursor' | 'vscode' | 'codex';
2
+ export declare const AGENTS: readonly Agent[];
3
+ /** The skill's folder: the one shipped in the package (skills/, copied in at pack time), else this checkout's. */
4
+ export declare function skillSource(): string;
5
+ /** How an agent starts the server: `volter mcp` when the command is on PATH, else this installation by its path. */
6
+ export declare function mcpCommand(): {
7
+ command: string;
8
+ args: string[];
9
+ };
10
+ /** The agents this machine (and this project) shows signs of. */
11
+ export declare function detectAgents(project: string): Agent[];
12
+ /** Install into each agent: what changed, one line each. */
13
+ export declare function installAgents(agents: Agent[], project: string): string[];
14
+ /** `volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes]`. */
15
+ export declare function agentsCommand(verb: string | undefined, args: string[], out: (s: string) => void): Promise<void>;
@@ -0,0 +1,206 @@
1
+ // `volter agents install` (docs/contributing/architecture.md, "The way in is the app's folder"): the skill
2
+ // (skills/volter-world) and the MCP server (`volter mcp`) put into the coding agents on this machine. Each change is
3
+ // idempotent and said; an entry another tool wrote is never touched: only our own `volter` entry and our own skill
4
+ // folder or marked block are written.
5
+ import { spawnSync } from 'node:child_process';
6
+ import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ export const AGENTS = ['claude', 'cursor', 'vscode', 'codex'];
11
+ const NAMES = { claude: 'Claude Code', cursor: 'Cursor', vscode: 'VS Code', codex: 'Codex' };
12
+ /** A command on PATH (with Windows' extensions), or null. */
13
+ function which(cmd) {
14
+ const exts = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';').map((e) => e.toLowerCase()) : [''];
15
+ for (const dir of (process.env.PATH ?? '').split(delimiter)) {
16
+ if (!dir)
17
+ continue;
18
+ for (const ext of ['', ...exts]) {
19
+ const p = join(dir, `${cmd}${ext}`);
20
+ if (existsSync(p))
21
+ return p;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ /** The skill's folder: the one shipped in the package (skills/, copied in at pack time), else this checkout's. */
27
+ export function skillSource() {
28
+ const here = dirname(fileURLToPath(import.meta.url));
29
+ for (const candidate of [resolve(here, '..', 'skills', 'volter-world'), resolve(here, '..', '..', 'skills', 'volter-world'), resolve(here, '..', '..', '..', 'skills', 'volter-world'), resolve(here, '..', '..', '..', '..', 'skills', 'volter-world')]) {
30
+ if (existsSync(join(candidate, 'SKILL.md')))
31
+ return candidate;
32
+ }
33
+ throw new Error('the volter-world skill is not in this installation (skills/volter-world)');
34
+ }
35
+ /** How an agent starts the server: `volter mcp` when the command is on PATH, else this installation by its path. */
36
+ export function mcpCommand() {
37
+ if (which('volter'))
38
+ return { command: 'volter', args: ['mcp'] };
39
+ const cli = fileURLToPath(new URL(`./cli${import.meta.url.endsWith('.ts') ? '.ts' : '.js'}`, import.meta.url));
40
+ return { command: process.execPath, args: [cli, 'mcp'] };
41
+ }
42
+ /** The agents this machine (and this project) shows signs of. */
43
+ export function detectAgents(project) {
44
+ const home = homedir();
45
+ const found = [];
46
+ if (existsSync(join(home, '.claude')) || which('claude'))
47
+ found.push('claude');
48
+ if (existsSync(join(home, '.cursor')) || which('cursor'))
49
+ found.push('cursor');
50
+ if (existsSync(join(project, '.vscode')) || which('code'))
51
+ found.push('vscode');
52
+ if (existsSync(join(home, '.codex')) || which('codex'))
53
+ found.push('codex');
54
+ return found;
55
+ }
56
+ /** A path as a person reads it: inside the project relative to it, inside the home folder from ~, with forward slashes. */
57
+ function shown(path) {
58
+ const within = (base) => { const r = relative(base, path); return r && !r.startsWith('..') && !isAbsolute(r) ? r.replace(/\\/g, '/') : null; };
59
+ const inProject = within(process.cwd());
60
+ if (inProject)
61
+ return inProject;
62
+ const inHome = within(homedir());
63
+ if (inHome)
64
+ return `~/${inHome}`;
65
+ return path;
66
+ }
67
+ const readJson = (path) => {
68
+ if (!existsSync(path))
69
+ return {};
70
+ const raw = readFileSync(path, 'utf8').trim();
71
+ if (!raw)
72
+ return {};
73
+ try {
74
+ const v = JSON.parse(raw);
75
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
76
+ }
77
+ catch {
78
+ throw new Error(`${path} is not JSON; not changing it (add the volter server by hand)`);
79
+ }
80
+ };
81
+ /** A config file replaced whole in one step (written beside it, then renamed over it): a reader never sees half of it. */
82
+ const writeAtomic = (path, text) => { mkdirSync(dirname(path), { recursive: true }); const tmp = `${path}.volter-${process.pid}.tmp`; writeFileSync(tmp, text); renameSync(tmp, path); };
83
+ const writeJson = (path, value) => writeAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
84
+ /** A command run as itself; on Windows, where `claude` is a .cmd that runs only in cmd, one line to cmd with each argument
85
+ * quoted (a path with a space stays one argument), as cross-spawn does. */
86
+ const runTool = (bin, args) => process.platform === 'win32'
87
+ ? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `"${[bin, ...args].map((a) => `"${a.replace(/"/g, '""')}"`).join(' ')}"`], { windowsHide: true, encoding: 'utf8', windowsVerbatimArguments: true })
88
+ : spawnSync(bin, args, { windowsHide: true, encoding: 'utf8' });
89
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
90
+ /** Our entry under `key` in a JSON config's server map: added, or already there; someone else's `volter` is left alone. */
91
+ function mergeServer(path, key, entry) {
92
+ const doc = readJson(path);
93
+ const servers = (doc[key] && typeof doc[key] === 'object' ? doc[key] : {});
94
+ const mine = servers.volter;
95
+ if (mine && same(mine, entry))
96
+ return `${shown(path)}: the volter server is already there`;
97
+ if (mine && !(typeof mine.command === 'string' && /volter|cli\.[tj]s/.test(`${mine.command} ${JSON.stringify(mine.args ?? [])}`)))
98
+ return `${shown(path)}: a "volter" server someone else configured is there; left alone`;
99
+ doc[key] = { ...servers, volter: entry };
100
+ writeJson(path, doc);
101
+ return `${shown(path)}: ${mine ? 'updated' : 'added'} the volter server`;
102
+ }
103
+ /** The skill copied into an agent's skills folder: our own folder, replaced whole. */
104
+ function copySkill(target) {
105
+ const dest = join(target, 'volter-world');
106
+ rmSync(dest, { recursive: true, force: true });
107
+ mkdirSync(target, { recursive: true });
108
+ cpSync(skillSource(), dest, { recursive: true });
109
+ return `${shown(dest)}: the volter-world skill`;
110
+ }
111
+ const BEGIN = '<!-- volter-world:begin -->';
112
+ const END = '<!-- volter-world:end -->';
113
+ /** The skill's instructions as a marked block in a Markdown instructions file; the rest of the file untouched. */
114
+ function markedBlock(path) {
115
+ const body = readFileSync(join(skillSource(), 'AGENTS.md'), 'utf8').trimEnd();
116
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
117
+ const start = existing.indexOf(BEGIN);
118
+ const end = existing.indexOf(END);
119
+ const outside = start >= 0 && end > start ? `${existing.slice(0, start)}${existing.slice(end + END.length)}`.replace(/\n{3,}$/, '\n\n') : existing;
120
+ const next = `${outside.trimEnd()}${outside.trim() ? '\n\n' : ''}${BEGIN}\n${body}\n${END}\n`;
121
+ if (next === existing)
122
+ return `${shown(path)}: the volter-world instructions are already there`;
123
+ writeAtomic(path, next);
124
+ return `${shown(path)}: ${start >= 0 ? 'updated' : 'added'} the volter-world instructions`;
125
+ }
126
+ /** Codex's config.toml: a [mcp_servers.volter] table appended when there is none; an existing one is left alone. */
127
+ function codexServer(path, entry) {
128
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
129
+ if (/^\s*\[mcp_servers\.(?:volter|"volter"|'volter')\]\s*$/m.test(existing))
130
+ return `${shown(path)}: a [mcp_servers.volter] table is already there; left alone`;
131
+ const q = (s) => JSON.stringify(s);
132
+ const table = `[mcp_servers.volter]\ncommand = ${q(entry.command)}\nargs = [${entry.args.map(q).join(', ')}]\n`;
133
+ mkdirSync(dirname(path), { recursive: true });
134
+ writeAtomic(path, `${existing.trimEnd()}${existing.trim() ? '\n\n' : ''}${table}`);
135
+ return `${shown(path)}: added the volter server`;
136
+ }
137
+ /** Claude Code: `claude mcp add` at user scope when the command is here (it owns its config file), else ~/.claude.json. */
138
+ function claudeServer(entry) {
139
+ const claude = which('claude');
140
+ if (claude) {
141
+ const has = runTool(claude, ['mcp', 'get', 'volter']);
142
+ if (has.status === 0)
143
+ return 'Claude Code: the volter server is already registered (claude mcp get volter)';
144
+ const add = runTool(claude, ['mcp', 'add', '--scope', 'user', 'volter', '--', entry.command, ...entry.args]);
145
+ if (add.status === 0)
146
+ return 'Claude Code: registered the volter server (claude mcp add --scope user volter)';
147
+ }
148
+ return mergeServer(join(homedir(), '.claude.json'), 'mcpServers', { type: 'stdio', command: entry.command, args: entry.args });
149
+ }
150
+ /** Install into each agent: what changed, one line each. */
151
+ export function installAgents(agents, project) {
152
+ const entry = mcpCommand();
153
+ const home = homedir();
154
+ const lines = [];
155
+ for (const a of agents) {
156
+ try {
157
+ if (a === 'claude') {
158
+ lines.push(copySkill(join(home, '.claude', 'skills')));
159
+ lines.push(claudeServer(entry));
160
+ }
161
+ if (a === 'cursor')
162
+ lines.push(mergeServer(join(home, '.cursor', 'mcp.json'), 'mcpServers', { command: entry.command, args: entry.args }));
163
+ if (a === 'vscode')
164
+ lines.push(mergeServer(join(project, '.vscode', 'mcp.json'), 'servers', { type: 'stdio', command: entry.command, args: entry.args }));
165
+ if (a === 'codex') {
166
+ lines.push(markedBlock(join(home, '.codex', 'AGENTS.md')));
167
+ lines.push(codexServer(join(home, '.codex', 'config.toml'), entry));
168
+ }
169
+ }
170
+ catch (e) {
171
+ lines.push(`${NAMES[a]}: not changed: ${e instanceof Error ? e.message : String(e)}`);
172
+ }
173
+ }
174
+ return lines;
175
+ }
176
+ /** `volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes]`. */
177
+ export async function agentsCommand(verb, args, out) {
178
+ if (verb !== 'install')
179
+ throw new Error('volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes]');
180
+ const project = process.cwd();
181
+ const named = AGENTS.filter((a) => args.includes(`--${a}`));
182
+ const agents = named.length ? named : detectAgents(project);
183
+ if (agents.length === 0) {
184
+ out('no coding agent found here (Claude Code, Cursor, VS Code, Codex); name one: volter agents install --claude');
185
+ process.exitCode = 1;
186
+ return;
187
+ }
188
+ const entry = mcpCommand();
189
+ out(`for ${agents.map((a) => NAMES[a]).join(', ')}\nskill volter-world\nserver ${[entry.command, ...entry.args].join(' ')}`);
190
+ if (!args.includes('--yes')) {
191
+ if (!process.stdin.isTTY) {
192
+ out('\nnothing changed: run again with --yes to install');
193
+ return;
194
+ }
195
+ process.stdout.write('\nInstall? [y/N] ');
196
+ const answer = await new Promise((done) => { process.stdin.once('data', (d) => done(String(d).trim().toLowerCase())); });
197
+ process.stdin.pause();
198
+ if (answer !== 'y' && answer !== 'yes') {
199
+ out('nothing changed');
200
+ return;
201
+ }
202
+ }
203
+ for (const line of installAgents(agents, project))
204
+ out(` ${line}`);
205
+ out('\nRestart the agent, then ask it to "set up Volter World for this app".');
206
+ }
package/dist/src/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--name <world>] [--allow-unknown] detect the app's vendors, write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] serve this world on a URL under /<org>/<world>/; prints the token and the console (its own origin, port p+1 by default)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n volter twin <vendor> mirror [--port <p>] the twin's UI mirror\n volter twin <vendor> conformance check the twin against the vendor's spec\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> --token <personal token> sign the CLI into the hosted platform (a personal token from the console)\n volter remote add <name> <url|path|org/world> [--token <token>] name a remote; org/world resolves through the platform you logged into\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n";
2
+ export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--install] [--name <world>] [--allow-unknown] detect the app's vendors (installing their twins when none are), write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] serve this world on a URL under /<org>/<world>/ for apps; prints the token and the console (on loopback: view's front, no token in the browser)\n volter world view [--port <p>] [--host <h>] [--no-open] [--no-origins] step into this world in a browser: its twins' own UIs, timeline, clock, branches (a non-loopback --host serves them on one origin)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n volter twin <vendor> mirror [--port <p>] the twin's UI mirror\n volter twin <vendor> conformance check the twin against the vendor's spec\n\nagents \u2014 your coding agent, driving the same verbs\n volter agents install [--claude] [--cursor] [--vscode] [--codex] [--yes] put the volter-world skill and the MCP server into the agents found here\n volter mcp the MCP server (stdio) an agent starts: the verbs above as tools\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> [--token <personal token>] sign the CLI into the hosted platform: approve it in the browser (or a token made under Account, for CI)\n volter whoami the platform the CLI is signed into, as whom, and its orgs\n volter logout revoke the CLI's token at the platform and forget it here\n volter remote add <name> <url|path|org/world> [--token <token>] [--create] name a remote; org/world (or world, in one org) resolves through the platform you logged into; --create makes it there from this world's twins\n volter open [<org>/<world>] [--no-open] a World's dashboard: a hosted one through the platform, else this world's (as view)\n volter completion bash|zsh|fish|powershell print a completion script for your shell\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n";