@volter/world 2.0.1 → 2.0.3

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,203 @@
1
+ // `volter mcp` (docs/contributing/architecture.md, "The way in is the app's folder"): an MCP server over stdio whose tools
2
+ // are the `volter` command's own verbs. A tool that reads (status, log, diff, branches) asks the SDK; a tool that acts runs
3
+ // the command itself, as a child, so there is one implementation of each verb and an agent gets what a person would.
4
+ // Locally a World is reached with its own token (the command finds it); the platform with the person's token from
5
+ // `volter login`, which only the person can approve, in their browser.
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { resolve } from 'node:path';
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import { z } from 'zod';
12
+ import { World } from "./world.js";
13
+ import { agentPrompt } from "./agent-prompt.js";
14
+ /** This command's own entry, beside this module (src/cli.ts in a checkout, dist/src/cli.js when published). */
15
+ const CLI = fileURLToPath(new URL(`./cli${import.meta.url.endsWith('.ts') ? '.ts' : '.js'}`, import.meta.url));
16
+ const MAX_OUTPUT = 20_000;
17
+ /** Run `volter <args>` in `dir` to its end (or its timeout), its stdout and stderr together, the tail kept. */
18
+ function volter(args, dir, timeoutSeconds = 600) {
19
+ return new Promise((done) => {
20
+ const child = spawn(process.execPath, [CLI, ...args], { cwd: dir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
21
+ let output = '';
22
+ const take = (d) => { output += d.toString(); if (output.length > MAX_OUTPUT * 2)
23
+ output = output.slice(-MAX_OUTPUT); };
24
+ child.stdout.on('data', take);
25
+ child.stderr.on('data', take);
26
+ const timer = setTimeout(() => { output += `\n(stopped after ${timeoutSeconds}s)`; child.kill(); }, timeoutSeconds * 1000);
27
+ child.on('error', (e) => { clearTimeout(timer); done({ code: 1, output: `${output}${e.message}` }); });
28
+ child.on('close', (code) => { clearTimeout(timer); done({ code: code ?? 1, output: output.length > MAX_OUTPUT ? `…${output.slice(-MAX_OUTPUT)}` : output }); });
29
+ });
30
+ }
31
+ /** A command that keeps running (the World's view, a sign-in waiting for approval): started once, answered as soon as
32
+ * its first lines say what the agent needs, left running for as long as this server runs. */
33
+ const background = new Map();
34
+ function startBackground(key, args, dir, ready, seconds = 60) {
35
+ background.get(key)?.kill();
36
+ background.delete(key);
37
+ return new Promise((done) => {
38
+ const child = spawn(process.execPath, [CLI, ...args], { cwd: dir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
39
+ background.set(key, child);
40
+ let output = '';
41
+ let answered = false;
42
+ const answer = (code) => { if (!answered) {
43
+ answered = true;
44
+ clearTimeout(timer);
45
+ done({ code, output });
46
+ } };
47
+ const take = (d) => { output += d.toString(); if (ready.test(output))
48
+ answer(0); };
49
+ child.stdout.on('data', take);
50
+ child.stderr.on('data', take);
51
+ child.on('error', (e) => { output += e.message; answer(1); });
52
+ child.on('close', (code) => { background.delete(key); answer(code ?? 1); });
53
+ const timer = setTimeout(() => answer(0), seconds * 1000);
54
+ });
55
+ }
56
+ /** Everything this server left running, ended: a World under a dashboard is brought down through its own command first
57
+ * (its state kept, its teardown recorded), then the command itself. */
58
+ function stopAll() {
59
+ for (const [key, child] of background) {
60
+ if (key.startsWith('view:'))
61
+ spawnSync(process.execPath, [CLI, 'world', 'down'], { cwd: key.slice('view:'.length), windowsHide: true, stdio: 'ignore', timeout: 60_000 });
62
+ child.kill();
63
+ }
64
+ background.clear();
65
+ }
66
+ for (const signal of ['SIGINT', 'SIGTERM'])
67
+ process.on(signal, () => { stopAll(); process.exit(0); });
68
+ process.on('exit', stopAll);
69
+ const text = (s, isError = false) => ({ content: [{ type: 'text', text: s.trim() || '(no output)' }], ...(isError ? { isError: true } : {}) });
70
+ const ran = (r) => text(r.output, r.code !== 0);
71
+ const asJson = (v) => text(JSON.stringify(v, null, 2));
72
+ const fail = (e) => text(e instanceof Error ? e.message : String(e), true);
73
+ const INSTRUCTIONS = `Volter World runs an app against twins of the SaaS APIs it calls (Stripe, Slack, GitHub, OpenAI and about a hundred more): the app keeps its real SDKs and code, and every write lands in a World that can be branched, reset and looked at on each vendor's own screens.
74
+
75
+ Work in the app's folder (each tool takes "dir", defaulting to where this server was started).
76
+ - Set up: world_init detects the vendors from the app's manifests and env names; show the person the list before keeping it. Then world_up.
77
+ - Run the app or its tests inside the World with world_run (never bare against the real vendors).
78
+ - Look: world_log and world_diff say what the app changed at each vendor; world_view starts the dashboard and returns its link for the person.
79
+ - Branch and reset like a database: world_branch, world_checkout, world_reset.
80
+ - Share with a team: platform_login returns a link and a code the PERSON must approve in their browser (you cannot approve it); after they do, remote_link links this World to <org>/<world> on the platform (create: true makes it there from this World's vendors); world_changeset cuts the changes with a message, and world_push sends them.
81
+ The guide: https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md`;
82
+ export async function serveMcp(version) {
83
+ const server = new McpServer({ name: 'volter', title: 'Volter World', version }, { instructions: INSTRUCTIONS });
84
+ const here = (dir) => resolve(dir ?? process.cwd());
85
+ const dirArg = { dir: z.string().optional().describe("The app's folder (the World's root). Defaults to where this server was started.") };
86
+ server.registerTool('world_status', {
87
+ title: 'World status', description: 'The World of the app folder: its branch, whether it is running, each twin\'s URL, its origin (the remote it pushes to) and unpushed changes.',
88
+ inputSchema: dirArg, annotations: { readOnlyHint: true },
89
+ }, async ({ dir }) => { try {
90
+ return asJson(World.open({ root: here(dir) }).status());
91
+ }
92
+ catch (e) {
93
+ return fail(e);
94
+ } });
95
+ server.registerTool('world_init', {
96
+ title: 'Set up a World for the app', description: "Detect the vendors the app calls (from its package manifests and env var names), install their twins when none are installed yet, and write .volter/world.json beside the code. Show the person the detected vendors; pass allowUnknown to keep going when some have no twin yet.",
97
+ inputSchema: { ...dirArg, name: z.string().optional().describe('The World\'s name (defaults to the folder\'s).'), allowUnknown: z.boolean().optional(), force: z.boolean().optional().describe('Overwrite an existing .volter/world.json.') },
98
+ }, async ({ dir, name, allowUnknown, force }) => ran(await volter(['world', 'init', '--install', ...(name ? ['--name', name] : []), ...(allowUnknown ? ['--allow-unknown'] : []), ...(force ? ['--force'] : [])], here(dir))));
99
+ server.registerTool('world_up', {
100
+ title: 'Start the World', description: 'Start the twins on the current branch. A fresh branch loads its default data (the story); a stopped one resumes where it was.',
101
+ inputSchema: { ...dirArg, seed: z.boolean().optional().describe('Load the default data on a fresh branch (default true).') },
102
+ }, async ({ dir, seed }) => ran(await volter(['world', 'up', ...(seed === false ? ['--no-seed'] : [])], here(dir))));
103
+ server.registerTool('world_run', {
104
+ title: 'Run a command in the World', description: "Run the app or its tests inside the World: the vendors' hosts go to their twins, so the real SDKs work unchanged. Returns the command's exit code and the tail of its output.",
105
+ inputSchema: { ...dirArg, command: z.array(z.string()).min(1).describe('The command and its arguments, e.g. ["npm", "test"].'), timeoutSeconds: z.number().int().positive().max(3600).optional() },
106
+ }, async ({ dir, command, timeoutSeconds }) => { const r = await volter(['world', 'run', '--', ...command], here(dir), timeoutSeconds ?? 600); return text(`exit ${r.code}\n${r.output}`, r.code !== 0); });
107
+ server.registerTool('world_down', {
108
+ title: 'Stop the World', description: 'Stop the twins. Their state is kept for the next world_up unless purge is true.',
109
+ inputSchema: { ...dirArg, purge: z.boolean().optional() }, annotations: { destructiveHint: true },
110
+ }, async ({ dir, purge }) => ran(await volter(['world', 'down', ...(purge ? ['--purge'] : [])], here(dir))));
111
+ server.registerTool('world_view', {
112
+ title: 'Open the dashboard', description: "Run the World with its dashboard on this machine (each vendor's own screens, the calls, the changes, the branches) and return its link for the person. The dashboard runs the World itself: a World already started with world_up is stopped first (its state is kept) and comes back under the dashboard. It keeps running while this server does; world_run works against it meanwhile.",
113
+ inputSchema: dirArg,
114
+ }, async ({ dir }) => {
115
+ const root = here(dir);
116
+ let before = '';
117
+ try {
118
+ const s = World.open({ root }).status();
119
+ if (s.running && !s.served) {
120
+ const down = await volter(['world', 'down'], root);
121
+ if (down.code !== 0)
122
+ return ran(down);
123
+ before = `${down.output.trim()} (state kept; the dashboard runs it now)\n`;
124
+ }
125
+ }
126
+ catch (e) {
127
+ return fail(e);
128
+ }
129
+ const r = await startBackground(`view:${root}`, ['world', 'view', '--no-open'], root, /console\s+\S+/);
130
+ return text(`${before}${r.output}`, r.code !== 0);
131
+ });
132
+ server.registerTool('world_log', {
133
+ title: 'What the app changed', description: 'Every write the app made in this World, newest last: vendor, operation and subject.',
134
+ inputSchema: { ...dirArg, limit: z.number().int().positive().max(1000).optional().describe('The newest this many (default 50).') }, annotations: { readOnlyHint: true },
135
+ }, async ({ dir, limit }) => { try {
136
+ const rows = World.open({ root: here(dir) }).log();
137
+ return asJson(rows.slice(-(limit ?? 50)).map((r) => ({ vendor: r.service, operation: r.operation ?? r.op, subject: r.subject, ...(r.receipt ? { receipt: r.receipt } : {}) })));
138
+ }
139
+ catch (e) {
140
+ return fail(e);
141
+ } });
142
+ server.registerTool('world_diff', {
143
+ title: 'Changes since the default data', description: 'How many changes each vendor has since the default data (or the last mark).',
144
+ inputSchema: dirArg, annotations: { readOnlyHint: true },
145
+ }, async ({ dir }) => { try {
146
+ const d = World.open({ root: here(dir) }).diff();
147
+ return asJson({ base: d.base, vendors: d.vendors });
148
+ }
149
+ catch (e) {
150
+ return fail(e);
151
+ } });
152
+ server.registerTool('world_branch', {
153
+ title: 'List or make branches', description: 'Without a name, the branches (the checked-out one first). With a name, make a branch from here and check it out, like a database branch: try something, then check out the old one or reset.',
154
+ inputSchema: { ...dirArg, name: z.string().optional() },
155
+ }, async ({ dir, name }) => {
156
+ if (name)
157
+ return ran(await volter(['world', 'branch', name], here(dir)));
158
+ try {
159
+ const w = World.open({ root: here(dir) });
160
+ return asJson({ current: w.name, branches: w.branches() });
161
+ }
162
+ catch (e) {
163
+ return fail(e);
164
+ }
165
+ });
166
+ server.registerTool('world_checkout', {
167
+ title: 'Switch branch', description: 'Check out another branch of the World.',
168
+ inputSchema: { ...dirArg, name: z.string() },
169
+ }, async ({ dir, name }) => ran(await volter(['world', 'checkout', name], here(dir))));
170
+ server.registerTool('world_reset', {
171
+ title: 'Back to the default data', description: "Discard the current branch's changes and load its default data again.",
172
+ inputSchema: dirArg, annotations: { destructiveHint: true },
173
+ }, async ({ dir }) => ran(await volter(['world', 'reset'], here(dir))));
174
+ server.registerTool('platform_login', {
175
+ title: 'Sign in to a platform', description: 'Start signing this machine\'s `volter` command in to a Volter World platform. Returns a link and a code: give both to the PERSON, who approves it in their browser (you cannot). The sign-in completes by itself once they do; check with platform_whoami.',
176
+ inputSchema: { url: z.string().url().describe("The platform's address (https://…), as the person gave it") },
177
+ }, async ({ url }) => ran(await startBackground('login', ['login', url, '--no-open'], process.cwd(), /code\s+[A-Z]{4}-[A-Z]{4}/, 30)));
178
+ server.registerTool('platform_whoami', {
179
+ title: 'Who the command is signed in as', description: 'The platform this machine\'s `volter` command is signed in to, as whom, and their orgs.',
180
+ inputSchema: {}, annotations: { readOnlyHint: true },
181
+ }, async () => ran(await volter(['whoami'], process.cwd())));
182
+ server.registerTool('remote_link', {
183
+ title: 'Link the World to the platform', description: "Link this World to <org>/<world> on the platform the command is signed in to, as the remote it pushes to. create: true makes that World on the platform from this World's vendors when it does not exist.",
184
+ inputSchema: { ...dirArg, target: z.string().regex(/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/).describe('<org>/<world>'), name: z.string().optional().describe('The remote\'s name (default origin).'), create: z.boolean().optional() },
185
+ }, async ({ dir, target, name, create }) => ran(await volter(['remote', 'add', name ?? 'origin', target, ...(create ? ['--create'] : [])], here(dir))));
186
+ server.registerTool('world_changeset', {
187
+ title: 'Cut a changeset', description: "Cut the World's unpushed changes into a reviewable changeset, with a message saying what and why; world_push sends it.",
188
+ inputSchema: { ...dirArg, message: z.string().min(1).describe('What the changes are and why.'), name: z.string().optional() },
189
+ }, async ({ dir, message, name }) => ran(await volter(['world', 'changeset', ...(name ? [name] : []), '-m', message], here(dir))));
190
+ server.registerTool('world_push', {
191
+ title: 'Push to the remote', description: "Send this World's unpushed changes to its remote (the platform's World after remote_link); the remote answers with a receipt for each change.",
192
+ inputSchema: { ...dirArg, changeset: z.string().optional().describe('Push one changeset by name; default all unpushed.') },
193
+ }, async ({ dir, changeset }) => ran(await volter(['world', 'push', ...(changeset ? [changeset] : [])], here(dir))));
194
+ server.registerPrompt('get-started', {
195
+ title: 'Set up Volter World for this app', description: 'Walks through setting up a World for the app in this folder, running its tests in it, and optionally sharing it on a platform.',
196
+ argsSchema: { platform: z.string().optional().describe("A platform to share the World on: its address"), world: z.string().optional().describe('<org>/<world> on that platform') },
197
+ }, ({ platform, world }) => ({ messages: [{ role: 'user', content: { type: 'text', text: agentPrompt({ ...(platform ? { platform } : {}), ...(world ? { world } : {}), tools: true }) } }] }));
198
+ // the client closing the connection (its stdin ends) ends this server, and the commands it left running with it
199
+ const stop = () => { stopAll(); process.exit(0); };
200
+ server.server.onclose = stop;
201
+ process.stdin.on('end', stop);
202
+ await server.connect(new StdioServerTransport());
203
+ }
@@ -0,0 +1,7 @@
1
+ /** This package's version, read from its own package.json. */
2
+ export declare function currentVersion(): string;
3
+ /** Whether `a` is a newer release than `b` (major.minor.patch; a prerelease is not offered). */
4
+ export declare function newer(a: string, b: string): boolean;
5
+ /** Start the day's check if one is due, while the command runs; the returned function, called when the command is
6
+ * done, waits a moment at most for it and prints the notice when there is one. Off, it does nothing and asks nothing. */
7
+ export declare function startUpdateCheck(args: string[], env?: NodeJS.ProcessEnv): () => Promise<void>;
@@ -0,0 +1,60 @@
1
+ // The update notice (docs/contributing/architecture.md, "The way in is the app's folder"): at most once a day the command
2
+ // asks the npm registry for @volter/world's latest version, while it does its own work, and says in one line on stderr
3
+ // when a newer one exists. Never in CI, never for --json, never when the person opted out (VOLTER_NO_UPDATE_NOTIFIER=1):
4
+ // then nothing is asked at all. The answer is kept beside the credentials, so a day's commands ask once.
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { credentialsPath } from "./credentials.js";
8
+ const DAY = 86_400_000;
9
+ const REGISTRY = 'https://registry.npmjs.org/@volter/world/latest';
10
+ /** This package's version, read from its own package.json. */
11
+ export function currentVersion() {
12
+ try {
13
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version ?? '0.0.0';
14
+ }
15
+ catch {
16
+ return '0.0.0';
17
+ }
18
+ }
19
+ /** Whether `a` is a newer release than `b` (major.minor.patch; a prerelease is not offered). */
20
+ export function newer(a, b) {
21
+ const parse = (v) => (/^\d+\.\d+\.\d+$/.test(v) ? v.split('.').map(Number) : null);
22
+ const x = parse(a);
23
+ const y = parse(b);
24
+ if (!x || !y)
25
+ return false;
26
+ for (let i = 0; i < 3; i++)
27
+ if (x[i] !== y[i])
28
+ return x[i] > y[i];
29
+ return false;
30
+ }
31
+ /** Start the day's check if one is due, while the command runs; the returned function, called when the command is
32
+ * done, waits a moment at most for it and prints the notice when there is one. Off, it does nothing and asks nothing. */
33
+ export function startUpdateCheck(args, env = process.env) {
34
+ if (env.VOLTER_NO_UPDATE_NOTIFIER || env.CI || args.includes('--json') || !process.stderr.isTTY)
35
+ return async () => undefined;
36
+ const path = join(dirname(credentialsPath()), 'update-check.json');
37
+ let kept = null;
38
+ try {
39
+ if (existsSync(path))
40
+ kept = JSON.parse(readFileSync(path, 'utf8'));
41
+ }
42
+ catch {
43
+ kept = null;
44
+ }
45
+ const due = !kept || Date.now() - Date.parse(kept.checkedAt) > DAY;
46
+ const check = due
47
+ ? fetch(REGISTRY, { signal: AbortSignal.timeout(1500) }).then(async (r) => (r.ok ? (await r.json()).version ?? null : null)).catch(() => null)
48
+ .then((latest) => { try {
49
+ mkdirSync(dirname(path), { recursive: true });
50
+ writeFileSync(path, `${JSON.stringify({ checkedAt: new Date().toISOString(), latest })}\n`);
51
+ }
52
+ catch { /* a read-only home only loses the cache */ } return latest; })
53
+ : Promise.resolve(kept?.latest ?? null);
54
+ return async () => {
55
+ const latest = await Promise.race([check, new Promise((r) => setTimeout(() => r(null), 800))]);
56
+ const here = currentVersion();
57
+ if (latest && newer(latest, here))
58
+ process.stderr.write(`\nvolter ${latest} is out (you have ${here}): npm install -g @volter/world\n`);
59
+ };
60
+ }
@@ -1,5 +1,5 @@
1
1
  import { rebaseChangeset, worldBootMarker, type Changeset, type LedgerDelta, type TwinAction, type WorldMarker, type ApplyReceipt, applyTwinWrite, type TwinResource } from '@volter/world-core';
2
- import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedWorld, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
2
+ import { approveWorldChangeset, createWorldChangeset, downWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, seedWorld, statusWorldChangeset, verifyWorldChangeset, type ServedWorld, type ViewConsole, type WorldView, type DeployTwinOutcome, type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance } from '@volter/world-runtime';
3
3
  import { rebaseBranch, type Receipt, type RootConfig } from '@volter/world-core';
4
4
  export type WorldRef = {
5
5
  name?: string;
@@ -198,6 +198,16 @@ export declare class World {
198
198
  console: string | null;
199
199
  }) => void;
200
200
  }): Promise<ServedWorld>;
201
+ /** `volter world view`: this World, its mirrors, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
202
+ view(opts?: {
203
+ port?: number;
204
+ host?: string;
205
+ console?: ViewConsole;
206
+ announce?: (line: string) => void;
207
+ origins?: boolean;
208
+ }): Promise<WorldView>;
209
+ /** The vendors this world has a twin of, as world.json names them (a twin service's package, else its id). */
210
+ vendors(): string[];
201
211
  /** The remotes named in world.json (`volter remote add`), name → url or path. */
202
212
  remotes(): Record<string, string>;
203
213
  /** Name a remote; `origin` is the one fetch and push use by default. A token given is remembered for it. */
package/dist/src/world.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { rebaseChangeset, worldBootMarker, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import clockForm from '@volter/world-core/world-clock';
8
- import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, materializeRoots } from '@volter/world-runtime';
8
+ import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, serveWorldView, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, materializeRoots } from '@volter/world-runtime';
9
9
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
10
  import { dirname, join, resolve } from 'node:path';
11
11
  import { requireToken, storeToken } from "./credentials.js";
@@ -94,7 +94,7 @@ export class World {
94
94
  if (opts.install !== false && !twins.every((t) => findInstalledPackage(dir, `@volter/twin-${t}`))) {
95
95
  // the runtime's own installer: bun under Bun, npm under Node — never assumed
96
96
  const cmd = typeof Bun !== 'undefined' ? ['bun', 'add', '-d'] : ['npm', 'install', '--save-dev'];
97
- const r = spawnSync(cmd[0], [...cmd.slice(1), ...twins.map((t) => `@volter/twin-${t}`)], { cwd: dir, stdio: 'inherit' });
97
+ const r = spawnSync(cmd[0], [...cmd.slice(1), ...twins.map((t) => `@volter/twin-${t}`)], { windowsHide: true, cwd: dir, stdio: 'inherit' });
98
98
  if (r.status !== 0)
99
99
  throw new Error(`installing the twins failed (${cmd.join(' ')} exited ${r.status})`);
100
100
  }
@@ -357,7 +357,14 @@ export class World {
357
357
  // ── serving ─────────────────────────────────────────────────────────────────────────────
358
358
  /** Serve this world on a port under `/<org>/<world>/`; returns when listening. */
359
359
  serve(opts = {}) { return serveWorld(this.name, { root: this.root, ...opts }); }
360
+ /** `volter world view`: this World, its mirrors, its branches and the console on one origin (docs/contributing/architecture.md, "Viewing a World"). */
361
+ view(opts = {}) { return serveWorldView(this.name, { root: this.root, ...opts }); }
360
362
  // ── remotes ─────────────────────────────────────────────────────────────────────────────
363
+ /** The vendors this world has a twin of, as world.json names them (a twin service's package, else its id). */
364
+ vendors() {
365
+ const { config } = loadWorldConfig(this.configRef(), this.root);
366
+ return [...new Set(config.services.filter((s) => (s.type ?? 'twin') === 'twin').map((s) => /^@volter\/twin-(.+)$/.exec(s.package ?? '')?.[1] ?? s.id))].sort();
367
+ }
361
368
  /** The remotes named in world.json (`volter remote add`), name → url or path. */
362
369
  remotes() { return { ...(loadWorldConfig(this.configRef(), this.root).config.remotes ?? {}) }; }
363
370
  /** Name a remote; `origin` is the one fetch and push use by default. A token given is remembered for it. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Run your app against a world of twins. `World` and `Repo` — one method per verb: init, up, run, log, diff, reset, branch, checkout, clone, fetch, changeset, push — and the `volter` command, one client of them.",
5
5
  "keywords": [
6
6
  "volter",
@@ -25,18 +25,32 @@
25
25
  "main": "dist/src/index.js",
26
26
  "files": [
27
27
  "src",
28
- "dist"
28
+ "dist",
29
+ "skills"
29
30
  ],
30
31
  "dependencies": {
31
- "@volter/world-core": "2.0.1",
32
- "@volter/world-runtime": "2.0.1"
32
+ "@modelcontextprotocol/sdk": "^1.30.1",
33
+ "@volter/world-core": "2.0.2",
34
+ "@volter/world-runtime": "2.0.3",
35
+ "zod": "^4.6.5"
36
+ },
37
+ "peerDependencies": {
38
+ "@volter/world-console": "2.0.3"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "@volter/world-console": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "devDependencies": {
46
+ "@volter/world-console": "2.0.3"
33
47
  },
34
48
  "bin": {
35
49
  "volter": "dist/src/cli.js"
36
50
  },
37
51
  "scripts": {
38
52
  "build": "node ../../scripts/publish/build.mjs",
39
- "prepack": "node ../../scripts/publish/prepare-publish.mjs prepack",
40
- "postpack": "node ../../scripts/publish/prepare-publish.mjs postpack"
53
+ "prepack": "node ./scripts/ship-skill.mjs in && node ../../scripts/publish/prepare-publish.mjs prepack",
54
+ "postpack": "node ../../scripts/publish/prepare-publish.mjs postpack && node ./scripts/ship-skill.mjs out"
41
55
  }
42
56
  }
@@ -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,35 @@
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
+
5
+ export type AgentPromptOptions = {
6
+ /** A platform to share the World on: its address. */
7
+ platform?: string;
8
+ /** The World on that platform, `<org>/<world>`; without it, the agent asks. */
9
+ world?: string;
10
+ /** Phrase the steps for an agent that has `volter mcp`'s tools rather than a shell. */
11
+ tools?: boolean;
12
+ };
13
+
14
+ /** The prompt, as plain text a person copies. */
15
+ export function agentPrompt(o: AgentPromptOptions = {}): string {
16
+ const cmd = (shell: string, tool: string): string => (o.tools ? `${tool} (\`${shell}\`)` : `\`${shell}\``);
17
+ const steps = [
18
+ '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.',
19
+ '',
20
+ o.tools
21
+ ? '1. You have the Volter World tools (volter mcp). If the `volter` command is missing, install it: `npm install -g @volter/world`.'
22
+ : '1. Install the command and your agent tooling: `npm install -g @volter/world`, then `volter agents install --yes`.',
23
+ `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.`,
24
+ `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.`,
25
+ `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'}).`,
26
+ ];
27
+ if (o.platform) {
28
+ const target = o.world ?? '<org>/<world>';
29
+ steps.push(
30
+ `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')}.`,
31
+ );
32
+ }
33
+ steps.push('', 'The guide: https://github.com/volter-ai/twin/blob/main/docs/guides/use-with-a-coding-agent.md');
34
+ return steps.join('\n');
35
+ }