@volter/world 3.0.164 → 3.0.166

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.
@@ -13,7 +13,7 @@ You need Node 22.6+, npm and registry access during installation. The browser pa
13
13
  Copy this starter into a new folder:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.164 example customer-onboarding --dir onboarding
16
+ $ npx @volter/world@3.0.166 example customer-onboarding --dir onboarding
17
17
  $ cd onboarding
18
18
  $ npm install
19
19
  ```
@@ -41,6 +41,8 @@ $ ./node_modules/.bin/volter world log
41
41
 
42
42
  Read the customer returned by Stripe, the signup row returned by SQL and the email returned by Resend. `completion: complete` means delivery was observed, not merely that a send request was accepted. Opaque IDs can differ between fresh Worlds; customer identity, stored fields and delivery status are the useful outcomes.
43
43
 
44
+ The SQL `completion` column retains the last send result. `run.mjs read` and the browser combine that saved row with Resend's current `last_event` to derive `complete` or `partial-email-bounced`. Observing delivery leaves the SQL row unchanged.
45
+
44
46
  ## Observe two different failures
45
47
 
46
48
  A refusing recipient's mail server creates a bounce after the send was accepted:
@@ -83,16 +85,29 @@ Then start the app through the World:
83
85
  $ ./node_modules/.bin/volter world run -- node server.mjs
84
86
  ```
85
87
 
86
- Open the printed app URL in your browser. Submit a signup, advance the World clock in another terminal and reload. The page shows the completion state and full stored outcome. Its server registers its app URL with the World after listening; consumers can discover it with `world app-url`.
88
+ The server registers its app URL with the World after listening. In another terminal, from the same application folder, read that endpoint with the installed operator command:
89
+
90
+ ```console
91
+ $ ./node_modules/.bin/volter-world app-url onboarding --root .
92
+ ```
93
+
94
+ `onboarding` is the World name selected by the init command above; use your current branch's name if you have branched. `--root .` selects this application's World. Open the returned URL or the server's printed app URL in your browser. Submit a signup, advance the World clock in another terminal and reload. The page shows the completion state and full stored outcome. [The CLI reference](https://world.volter.ai/docs/reference/cli.html#the-world-itself) describes endpoint discovery and registration.
95
+
96
+ To have the World start and stop the app with its services, declare `server.mjs` as a process service using [run a full stack](https://world.volter.ai/docs/guides/run-a-full-stack.html#the-app-as-a-service). This server honors the World-assigned `PORT`; use `/` as its HTTP readiness path. The manual command above chooses an available port when `PORT` is absent.
87
97
 
88
- To open the World dashboard, stop the app with Ctrl-C first, then switch the owned compute to the viewer:
98
+ To open the World dashboard, switch the owned compute to the viewer. If you started the app
99
+ manually with `world run`, stop that app with Ctrl-C first. If you declared it as a process
100
+ service, the `down` command below stops it with the World's other services:
89
101
 
90
102
  ```console
91
103
  $ ./node_modules/.bin/volter world down
92
104
  $ ./node_modules/.bin/volter world view --no-open
93
105
  ```
94
106
 
95
- The viewer prints the dashboard URL and keeps running. Start `world run -- node server.mjs` in a second terminal if you want the application alongside it. Open **Stripe** under **Records**, then select the customer to inspect its name, email and stored ID. Open **Resend** to inspect the accepted email and delivery event. **Board** opens the vendor pages declared by the installed twin; its root workspace may cover settings rather than customers. Provisioning remains in full history even when hidden from the business-activity view. Read the application's SQL completion record with `run.mjs read` or its browser page.
107
+ The viewer prints the dashboard URL and keeps running. In manual app mode, start
108
+ `world run -- node server.mjs` in a second terminal if you want the application alongside it.
109
+ With a declared process service, the viewer starts the app as part of the World; no second
110
+ app command is needed. Open **Stripe** under **Records**, then select the customer to inspect its name, email and stored ID. Open **Resend** to inspect the accepted email and delivery event. **Board** opens the vendor pages declared by the installed twin; its root workspace may cover settings rather than customers. Provisioning remains in full history even when hidden from the business-activity view. Read the application's combined signup, customer and delivery result with `run.mjs read` or its browser page.
96
111
 
97
112
  Stop app consumers before stopping the viewer. `down` retains vendor and declared application state. [Resume](https://world.volter.ai/docs/guides/resume-a-world.html) explains continuing it; [demo a workflow](https://world.volter.ai/docs/guides/demo-a-workflow.html) uses this same application for a repeatable baseline, refusal and retry. Reset only your disposable example World; it discards that branch's work.
98
113
 
@@ -11,11 +11,12 @@
11
11
  "pg": "8.16.3"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.164",
15
- "@volter/world-core": "3.0.154",
16
- "@volter/world-runtime": "3.0.162",
17
- "@volter/world-console": "3.0.150",
14
+ "@volter/world": "3.0.166",
15
+ "@volter/world-core": "3.0.155",
16
+ "@volter/world-runtime": "3.0.163",
17
+ "@volter/world-console": "3.0.151",
18
18
  "@volter/twin-stripe": "3.0.2",
19
- "@volter/twin-resend": "1.0.1"
19
+ "@volter/twin-resend": "1.0.1",
20
+ "@volter/world-ui": "1.0.155"
20
21
  }
21
22
  }
@@ -25,7 +25,7 @@ const server = createServer(async (request, response) => {
25
25
  response.end(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Customer onboarding</title><link rel="stylesheet" href="/brand/tokens.css"><link rel="stylesheet" href="/style.css"><main><header><img src="/brand/world.svg" width="32" height="32" alt="Volter World"><h1>Customer onboarding</h1></header><p>Create a customer, save an application row and request a welcome email.</p><form method="post" action="/signup"><label>Name <input name="name" value="Ada" required></label><label>Email <input name="email" type="email" value="owner@world.test" required></label><button>Create account</button></form><h2>Accounts</h2><p>Reload after advancing the World clock to observe delivery. A refused email leaves the customer and application row intact.</p>${rows.map(r => `<article><h3>${escape(r.name)}</h3><p>${escape(r.email)} · <strong>${escape(r.completion)}</strong></p><p>Stripe: <code>${escape(r.customer_id)}</code> · Email: <code>${escape(r.email_id ?? 'not accepted')}</code></p><details><summary>Stored outcome</summary><pre>${escape(JSON.stringify(r, null, 2))}</pre></details></article>`).join('') || '<p>No customers yet.</p>'}</main></html>`);
26
26
  } catch (error) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end(error.message); }
27
27
  });
28
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
28
+ await new Promise(resolve => server.listen(Number(process.env.PORT ?? 0), '127.0.0.1', resolve));
29
29
  const url = `http://127.0.0.1:${server.address().port}`;
30
30
  setAppUrl(process.env.VOLTER_WORLD, url, { root: process.cwd() });
31
31
  console.log(`Customer onboarding ${url}`);
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
13
13
  Use an empty parent folder. Create the baseline starter there:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.164 example integration-branches --dir baseline
16
+ $ npx @volter/world@3.0.166 example integration-branches --dir baseline
17
17
  $ cd baseline
18
18
  $ npm install
19
19
  $ ./node_modules/.bin/volter world init --name baseline --twins stripe
@@ -38,9 +38,9 @@ The stopped baseline retains its history and can supply local path remotes. Defa
38
38
  Create three more copies in the same parent folder:
39
39
 
40
40
  ```console
41
- $ npx @volter/world@3.0.164 example integration-branches --dir integration
42
- $ npx @volter/world@3.0.164 example integration-branches --dir worker-a
43
- $ npx @volter/world@3.0.164 example integration-branches --dir worker-b
41
+ $ npx @volter/world@3.0.166 example integration-branches --dir integration
42
+ $ npx @volter/world@3.0.166 example integration-branches --dir worker-a
43
+ $ npx @volter/world@3.0.166 example integration-branches --dir worker-b
44
44
  ```
45
45
 
46
46
  In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
@@ -9,10 +9,11 @@
9
9
  "stripe": "17.7.0"
10
10
  },
11
11
  "devDependencies": {
12
- "@volter/world": "3.0.164",
13
- "@volter/world-core": "3.0.154",
14
- "@volter/world-runtime": "3.0.162",
15
- "@volter/world-console": "3.0.150",
16
- "@volter/twin-stripe": "3.0.2"
12
+ "@volter/world": "3.0.166",
13
+ "@volter/world-core": "3.0.155",
14
+ "@volter/world-runtime": "3.0.163",
15
+ "@volter/world-console": "3.0.151",
16
+ "@volter/twin-stripe": "3.0.2",
17
+ "@volter/world-ui": "1.0.155"
17
18
  }
18
19
  }
@@ -19,7 +19,7 @@ A catalog page may select a newer default. Keep this example's manifest and inst
19
19
  together; its recorded workflow does not establish behavior for a different release.
20
20
 
21
21
  ```console
22
- $ npx @volter/world@3.0.164 example workplace-agent --dir workplace
22
+ $ npx @volter/world@3.0.166 example workplace-agent --dir workplace
23
23
  $ cd workplace
24
24
  $ npm install
25
25
  $ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
@@ -11,12 +11,13 @@
11
11
  "@slack/web-api": "7.19.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.164",
15
- "@volter/world-core": "3.0.154",
16
- "@volter/world-runtime": "3.0.162",
17
- "@volter/world-console": "3.0.150",
14
+ "@volter/world": "3.0.166",
15
+ "@volter/world-core": "3.0.155",
16
+ "@volter/world-runtime": "3.0.163",
17
+ "@volter/world-console": "3.0.151",
18
18
  "@volter/twin-github": "3.0.9",
19
19
  "@volter/twin-linear": "3.0.5",
20
- "@volter/twin-slack": "3.0.8"
20
+ "@volter/twin-slack": "3.0.8",
21
+ "@volter/world-ui": "1.0.155"
21
22
  }
22
23
  }
@@ -2,7 +2,7 @@ export type Agent = 'claude' | 'cursor' | 'vscode' | 'codex';
2
2
  export declare const AGENTS: readonly Agent[];
3
3
  /** The skill's folder: the one shipped in the package (skills/, copied in at pack time), else this checkout's. */
4
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. */
5
+ /** Start this installation's server under this runtime; PATH may contain another version of `volter`. */
6
6
  export declare function mcpCommand(): {
7
7
  command: string;
8
8
  args: string[];
@@ -32,10 +32,8 @@ export function skillSource() {
32
32
  }
33
33
  throw new Error('the volter-world skill is not in this installation (skills/volter-world)');
34
34
  }
35
- /** How an agent starts the server: `volter mcp` when the command is on PATH, else this installation by its path. */
35
+ /** Start this installation's server under this runtime; PATH may contain another version of `volter`. */
36
36
  export function mcpCommand() {
37
- if (which('volter'))
38
- return { command: 'volter', args: ['mcp'] };
39
37
  const cli = fileURLToPath(new URL(`./cli${import.meta.url.endsWith('.ts') ? '.ts' : '.js'}`, import.meta.url));
40
38
  return { command: process.execPath, args: [cli, 'mcp'] };
41
39
  }
package/dist/src/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // cwd; `volter twin <vendor> …` runs one twin on its own; `volter remote …` is the hosted remote.
5
5
  // One client of `World`; it adds nothing the SDK does not have.
6
6
  import { detectRepoVendors, errorMessage, formatInitReport, resolveCatalog, findInstalledPackage, packCli, serveConsoleFor, catalogDefaults, CATALOG_INDEX_PACKAGE } from '@volter/world-runtime';
7
- import { readFileSync, realpathSync } from 'node:fs';
7
+ import { realpathSync } from 'node:fs';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { spawn, spawnSync } from 'node:child_process';
10
10
  import { createHash } from 'node:crypto';
@@ -15,7 +15,7 @@ import { forgetPlatform, forgetToken, platformOrigin, storePlatform, storeToken,
15
15
  import { parseOriginUrl, World } from "./world.js";
16
16
  import { copyExample, examples } from "./examples.js";
17
17
  import { waitForServeShutdown } from "./serve-shutdown.js";
18
- import { startUpdateCheck } from "./update-notice.js";
18
+ import { currentVersion, startUpdateCheck } from "./update-notice.js";
19
19
  import { withoutVaultSignIn } from '@volter/world-runtime';
20
20
  /** Open a URL in the person's browser; a machine without one only loses the convenience. */
21
21
  function openInBrowser(url) {
@@ -1095,16 +1095,7 @@ async function offerAgents() {
1095
1095
  }
1096
1096
  /** This package's version, for the MCP server's handshake. */
1097
1097
  function ownVersion() {
1098
- // src/cli.ts in the repository, dist/src/cli.js in the published package: the package's manifest is one or two up
1099
- for (const up of ['../package.json', '../../package.json']) {
1100
- try {
1101
- const m = JSON.parse(readFileSync(fileURLToPath(new URL(up, import.meta.url)), 'utf8'));
1102
- if (m.name === '@volter/world' && m.version)
1103
- return m.version;
1104
- }
1105
- catch { /* not there */ }
1106
- }
1107
- return '0.0.0';
1098
+ return currentVersion() ?? '0.0.0';
1108
1099
  }
1109
1100
  async function main() {
1110
1101
  const [noun, verb, ...args] = process.argv.slice(2);
package/dist/src/mcp.js CHANGED
@@ -29,9 +29,9 @@ function volter(args, dir, timeoutSeconds = 600) {
29
29
  });
30
30
  }
31
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. */
32
+ * its announcement says what the agent needs, left running for as long as this server runs. */
33
33
  const background = new Map();
34
- function startBackground(key, args, dir, ready, seconds = 60) {
34
+ function startBackground(key, args, dir, ready, seconds = 60, incompleteCode = 0) {
35
35
  background.get(key)?.kill();
36
36
  background.delete(key);
37
37
  return new Promise((done) => {
@@ -44,15 +44,23 @@ function startBackground(key, args, dir, ready, seconds = 60) {
44
44
  clearTimeout(timer);
45
45
  done({ code, output });
46
46
  } };
47
- const take = (d) => { output += d.toString(); if (ready.test(output))
47
+ const take = (d) => { output += d.toString(); if (typeof ready === 'function' ? ready(output) : ready.test(output))
48
48
  answer(0); };
49
49
  child.stdout.on('data', take);
50
50
  child.stderr.on('data', take);
51
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);
52
+ child.on('close', (code) => { background.delete(key); answer(code === 0 ? incompleteCode : code ?? 1); });
53
+ const timer = setTimeout(() => answer(incompleteCode), seconds * 1000);
54
54
  });
55
55
  }
56
+ /** cli.ts finishes the access announcement after the runtime's initial viewing/console lines. */
57
+ function viewAnnouncementComplete(output) {
58
+ if (/(?:^|\n)apps[ \t]+/.test(output)) {
59
+ // The local-origin announcement ends with its browser guidance, after any missing-console notice.
60
+ return /(?:^|\n)\(the World opens at its own \*\.localhost origin;[^\r\n]*\)\r?\n/.test(output);
61
+ }
62
+ return /(?:^|\n)(?:read-only view[ \t]+\S+|\(no console installed:[^\r\n]*\))\r?\n/.test(output);
63
+ }
56
64
  /** Everything this server left running, ended: a World under a dashboard is brought down through its own command first
57
65
  * (its state kept, its teardown recorded), then the command itself. */
58
66
  function stopAll() {
@@ -126,7 +134,7 @@ export async function serveMcp(version) {
126
134
  catch (e) {
127
135
  return fail(e);
128
136
  }
129
- const r = await startBackground(`view:${root}`, ['world', 'view', '--no-open'], root, /console\s+\S+/);
137
+ const r = await startBackground(`view:${root}`, ['world', 'view', '--no-open'], root, viewAnnouncementComplete, 60, 1);
130
138
  return text(`${before}${r.output}`, r.code !== 0);
131
139
  });
132
140
  server.registerTool('world_log', {
@@ -1,5 +1,5 @@
1
- /** This package's version, read from its own package.json. */
2
- export declare function currentVersion(): string;
1
+ /** This package's version, read from its own package.json, or null when it cannot be established. */
2
+ export declare function currentVersion(): string | null;
3
3
  /** Whether `a` is a newer release than `b` (major.minor.patch; a prerelease is not offered). */
4
4
  export declare function newer(a: string, b: string): boolean;
5
5
  /** Start the day's check if one is due, while the command runs; the returned function, called when the command is
@@ -7,14 +7,18 @@ import { dirname, join } from 'node:path';
7
7
  import { credentialsPath } from "./credentials.js";
8
8
  const DAY = 86_400_000;
9
9
  const REGISTRY = 'https://registry.npmjs.org/@volter/world/latest';
10
- /** This package's version, read from its own package.json. */
10
+ /** This package's version, read from its own package.json, or null when it cannot be established. */
11
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';
12
+ // src/update-notice.ts in the repository, dist/src/update-notice.js in the published package
13
+ for (const up of ['../package.json', '../../package.json']) {
14
+ try {
15
+ const m = JSON.parse(readFileSync(new URL(up, import.meta.url), 'utf8'));
16
+ if (m.name === '@volter/world' && typeof m.version === 'string' && m.version)
17
+ return m.version;
18
+ }
19
+ catch { /* not there or unreadable */ }
17
20
  }
21
+ return null;
18
22
  }
19
23
  /** Whether `a` is a newer release than `b` (major.minor.patch; a prerelease is not offered). */
20
24
  export function newer(a, b) {
@@ -33,6 +37,9 @@ export function newer(a, b) {
33
37
  export function startUpdateCheck(args, env = process.env) {
34
38
  if (env.VOLTER_NO_UPDATE_NOTIFIER || env.CI || args.includes('--json') || !process.stderr.isTTY)
35
39
  return async () => undefined;
40
+ const here = currentVersion();
41
+ if (!here)
42
+ return async () => undefined;
36
43
  const path = join(dirname(credentialsPath()), 'update-check.json');
37
44
  let kept = null;
38
45
  try {
@@ -53,7 +60,6 @@ export function startUpdateCheck(args, env = process.env) {
53
60
  : Promise.resolve(kept?.latest ?? null);
54
61
  return async () => {
55
62
  const latest = await Promise.race([check, new Promise((r) => setTimeout(() => r(null), 800))]);
56
- const here = currentVersion();
57
63
  if (latest && newer(latest, here))
58
64
  process.stderr.write(`\nvolter ${latest} is out (you have ${here}): npm install -g @volter/world\n`);
59
65
  };
@@ -13,7 +13,7 @@ You need Node 22.6+, npm and registry access during installation. The browser pa
13
13
  Copy this starter into a new folder:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.164 example customer-onboarding --dir onboarding
16
+ $ npx @volter/world@3.0.166 example customer-onboarding --dir onboarding
17
17
  $ cd onboarding
18
18
  $ npm install
19
19
  ```
@@ -41,6 +41,8 @@ $ ./node_modules/.bin/volter world log
41
41
 
42
42
  Read the customer returned by Stripe, the signup row returned by SQL and the email returned by Resend. `completion: complete` means delivery was observed, not merely that a send request was accepted. Opaque IDs can differ between fresh Worlds; customer identity, stored fields and delivery status are the useful outcomes.
43
43
 
44
+ The SQL `completion` column retains the last send result. `run.mjs read` and the browser combine that saved row with Resend's current `last_event` to derive `complete` or `partial-email-bounced`. Observing delivery leaves the SQL row unchanged.
45
+
44
46
  ## Observe two different failures
45
47
 
46
48
  A refusing recipient's mail server creates a bounce after the send was accepted:
@@ -83,16 +85,29 @@ Then start the app through the World:
83
85
  $ ./node_modules/.bin/volter world run -- node server.mjs
84
86
  ```
85
87
 
86
- Open the printed app URL in your browser. Submit a signup, advance the World clock in another terminal and reload. The page shows the completion state and full stored outcome. Its server registers its app URL with the World after listening; consumers can discover it with `world app-url`.
88
+ The server registers its app URL with the World after listening. In another terminal, from the same application folder, read that endpoint with the installed operator command:
89
+
90
+ ```console
91
+ $ ./node_modules/.bin/volter-world app-url onboarding --root .
92
+ ```
93
+
94
+ `onboarding` is the World name selected by the init command above; use your current branch's name if you have branched. `--root .` selects this application's World. Open the returned URL or the server's printed app URL in your browser. Submit a signup, advance the World clock in another terminal and reload. The page shows the completion state and full stored outcome. [The CLI reference](https://world.volter.ai/docs/reference/cli.html#the-world-itself) describes endpoint discovery and registration.
95
+
96
+ To have the World start and stop the app with its services, declare `server.mjs` as a process service using [run a full stack](https://world.volter.ai/docs/guides/run-a-full-stack.html#the-app-as-a-service). This server honors the World-assigned `PORT`; use `/` as its HTTP readiness path. The manual command above chooses an available port when `PORT` is absent.
87
97
 
88
- To open the World dashboard, stop the app with Ctrl-C first, then switch the owned compute to the viewer:
98
+ To open the World dashboard, switch the owned compute to the viewer. If you started the app
99
+ manually with `world run`, stop that app with Ctrl-C first. If you declared it as a process
100
+ service, the `down` command below stops it with the World's other services:
89
101
 
90
102
  ```console
91
103
  $ ./node_modules/.bin/volter world down
92
104
  $ ./node_modules/.bin/volter world view --no-open
93
105
  ```
94
106
 
95
- The viewer prints the dashboard URL and keeps running. Start `world run -- node server.mjs` in a second terminal if you want the application alongside it. Open **Stripe** under **Records**, then select the customer to inspect its name, email and stored ID. Open **Resend** to inspect the accepted email and delivery event. **Board** opens the vendor pages declared by the installed twin; its root workspace may cover settings rather than customers. Provisioning remains in full history even when hidden from the business-activity view. Read the application's SQL completion record with `run.mjs read` or its browser page.
107
+ The viewer prints the dashboard URL and keeps running. In manual app mode, start
108
+ `world run -- node server.mjs` in a second terminal if you want the application alongside it.
109
+ With a declared process service, the viewer starts the app as part of the World; no second
110
+ app command is needed. Open **Stripe** under **Records**, then select the customer to inspect its name, email and stored ID. Open **Resend** to inspect the accepted email and delivery event. **Board** opens the vendor pages declared by the installed twin; its root workspace may cover settings rather than customers. Provisioning remains in full history even when hidden from the business-activity view. Read the application's combined signup, customer and delivery result with `run.mjs read` or its browser page.
96
111
 
97
112
  Stop app consumers before stopping the viewer. `down` retains vendor and declared application state. [Resume](https://world.volter.ai/docs/guides/resume-a-world.html) explains continuing it; [demo a workflow](https://world.volter.ai/docs/guides/demo-a-workflow.html) uses this same application for a repeatable baseline, refusal and retry. Reset only your disposable example World; it discards that branch's work.
98
113
 
@@ -11,11 +11,12 @@
11
11
  "pg": "8.16.3"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.164",
15
- "@volter/world-core": "3.0.154",
16
- "@volter/world-runtime": "3.0.162",
17
- "@volter/world-console": "3.0.150",
14
+ "@volter/world": "3.0.166",
15
+ "@volter/world-core": "3.0.155",
16
+ "@volter/world-runtime": "3.0.163",
17
+ "@volter/world-console": "3.0.151",
18
18
  "@volter/twin-stripe": "3.0.2",
19
- "@volter/twin-resend": "1.0.1"
19
+ "@volter/twin-resend": "1.0.1",
20
+ "@volter/world-ui": "1.0.155"
20
21
  }
21
22
  }
@@ -25,7 +25,7 @@ const server = createServer(async (request, response) => {
25
25
  response.end(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Customer onboarding</title><link rel="stylesheet" href="/brand/tokens.css"><link rel="stylesheet" href="/style.css"><main><header><img src="/brand/world.svg" width="32" height="32" alt="Volter World"><h1>Customer onboarding</h1></header><p>Create a customer, save an application row and request a welcome email.</p><form method="post" action="/signup"><label>Name <input name="name" value="Ada" required></label><label>Email <input name="email" type="email" value="owner@world.test" required></label><button>Create account</button></form><h2>Accounts</h2><p>Reload after advancing the World clock to observe delivery. A refused email leaves the customer and application row intact.</p>${rows.map(r => `<article><h3>${escape(r.name)}</h3><p>${escape(r.email)} · <strong>${escape(r.completion)}</strong></p><p>Stripe: <code>${escape(r.customer_id)}</code> · Email: <code>${escape(r.email_id ?? 'not accepted')}</code></p><details><summary>Stored outcome</summary><pre>${escape(JSON.stringify(r, null, 2))}</pre></details></article>`).join('') || '<p>No customers yet.</p>'}</main></html>`);
26
26
  } catch (error) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end(error.message); }
27
27
  });
28
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
28
+ await new Promise(resolve => server.listen(Number(process.env.PORT ?? 0), '127.0.0.1', resolve));
29
29
  const url = `http://127.0.0.1:${server.address().port}`;
30
30
  setAppUrl(process.env.VOLTER_WORLD, url, { root: process.cwd() });
31
31
  console.log(`Customer onboarding ${url}`);
@@ -13,7 +13,7 @@ Give two workers the same Stripe baseline, let them change different existing cu
13
13
  Use an empty parent folder. Create the baseline starter there:
14
14
 
15
15
  ```console
16
- $ npx @volter/world@3.0.164 example integration-branches --dir baseline
16
+ $ npx @volter/world@3.0.166 example integration-branches --dir baseline
17
17
  $ cd baseline
18
18
  $ npm install
19
19
  $ ./node_modules/.bin/volter world init --name baseline --twins stripe
@@ -38,9 +38,9 @@ The stopped baseline retains its history and can supply local path remotes. Defa
38
38
  Create three more copies in the same parent folder:
39
39
 
40
40
  ```console
41
- $ npx @volter/world@3.0.164 example integration-branches --dir integration
42
- $ npx @volter/world@3.0.164 example integration-branches --dir worker-a
43
- $ npx @volter/world@3.0.164 example integration-branches --dir worker-b
41
+ $ npx @volter/world@3.0.166 example integration-branches --dir integration
42
+ $ npx @volter/world@3.0.166 example integration-branches --dir worker-a
43
+ $ npx @volter/world@3.0.166 example integration-branches --dir worker-b
44
44
  ```
45
45
 
46
46
  In `integration`, then `worker-a`, then `worker-b`, install dependencies and initialize with the folder's name. Before a first boot or app write, link and pull the baseline:
@@ -9,10 +9,11 @@
9
9
  "stripe": "17.7.0"
10
10
  },
11
11
  "devDependencies": {
12
- "@volter/world": "3.0.164",
13
- "@volter/world-core": "3.0.154",
14
- "@volter/world-runtime": "3.0.162",
15
- "@volter/world-console": "3.0.150",
16
- "@volter/twin-stripe": "3.0.2"
12
+ "@volter/world": "3.0.166",
13
+ "@volter/world-core": "3.0.155",
14
+ "@volter/world-runtime": "3.0.163",
15
+ "@volter/world-console": "3.0.151",
16
+ "@volter/twin-stripe": "3.0.2",
17
+ "@volter/world-ui": "1.0.155"
17
18
  }
18
19
  }
@@ -19,7 +19,7 @@ A catalog page may select a newer default. Keep this example's manifest and inst
19
19
  together; its recorded workflow does not establish behavior for a different release.
20
20
 
21
21
  ```console
22
- $ npx @volter/world@3.0.164 example workplace-agent --dir workplace
22
+ $ npx @volter/world@3.0.166 example workplace-agent --dir workplace
23
23
  $ cd workplace
24
24
  $ npm install
25
25
  $ ./node_modules/.bin/volter world init --name workplace --twins github,linear,slack
@@ -11,12 +11,13 @@
11
11
  "@slack/web-api": "7.19.0"
12
12
  },
13
13
  "devDependencies": {
14
- "@volter/world": "3.0.164",
15
- "@volter/world-core": "3.0.154",
16
- "@volter/world-runtime": "3.0.162",
17
- "@volter/world-console": "3.0.150",
14
+ "@volter/world": "3.0.166",
15
+ "@volter/world-core": "3.0.155",
16
+ "@volter/world-runtime": "3.0.163",
17
+ "@volter/world-console": "3.0.151",
18
18
  "@volter/twin-github": "3.0.9",
19
19
  "@volter/twin-linear": "3.0.5",
20
- "@volter/twin-slack": "3.0.8"
20
+ "@volter/twin-slack": "3.0.8",
21
+ "@volter/world-ui": "1.0.155"
21
22
  }
22
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world",
3
- "version": "3.0.164",
3
+ "version": "3.0.166",
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",
@@ -31,12 +31,12 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.30.1",
34
- "@volter/world-core": "3.0.154",
35
- "@volter/world-runtime": "3.0.162",
34
+ "@volter/world-core": "3.0.155",
35
+ "@volter/world-runtime": "3.0.163",
36
36
  "zod": "^4.6.5"
37
37
  },
38
38
  "peerDependencies": {
39
- "@volter/world-console": "3.0.150"
39
+ "@volter/world-console": "3.0.151"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@volter/world-console": {
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@volter/world-console": "3.0.150"
47
+ "@volter/world-console": "3.0.151"
48
48
  },
49
49
  "bin": {
50
50
  "volter": "dist/src/cli.js"
package/src/agents.ts CHANGED
@@ -31,9 +31,8 @@ export function skillSource(): string {
31
31
  throw new Error('the volter-world skill is not in this installation (skills/volter-world)');
32
32
  }
33
33
 
34
- /** How an agent starts the server: `volter mcp` when the command is on PATH, else this installation by its path. */
34
+ /** Start this installation's server under this runtime; PATH may contain another version of `volter`. */
35
35
  export function mcpCommand(): { command: string; args: string[] } {
36
- if (which('volter')) return { command: 'volter', args: ['mcp'] };
37
36
  const cli = fileURLToPath(new URL(`./cli${import.meta.url.endsWith('.ts') ? '.ts' : '.js'}`, import.meta.url));
38
37
  return { command: process.execPath, args: [cli, 'mcp'] };
39
38
  }
package/src/cli.ts CHANGED
@@ -16,7 +16,7 @@ import { forgetPlatform, forgetToken, platformOrigin, requireToken, storePlatfor
16
16
  import { parseOriginUrl, World } from './world.ts';
17
17
  import { copyExample, examples } from './examples.ts';
18
18
  import { waitForServeShutdown } from './serve-shutdown.ts';
19
- import { startUpdateCheck } from './update-notice.ts';
19
+ import { currentVersion, startUpdateCheck } from './update-notice.ts';
20
20
  import type { ViewConsole } from '@volter/world-runtime';
21
21
  import { withoutVaultSignIn } from '@volter/world-runtime';
22
22
 
@@ -814,11 +814,7 @@ async function offerAgents(): Promise<void> {
814
814
 
815
815
  /** This package's version, for the MCP server's handshake. */
816
816
  function ownVersion(): string {
817
- // src/cli.ts in the repository, dist/src/cli.js in the published package: the package's manifest is one or two up
818
- for (const up of ['../package.json', '../../package.json']) {
819
- try { const m = JSON.parse(readFileSync(fileURLToPath(new URL(up, import.meta.url)), 'utf8')) as { name?: string; version?: string }; if (m.name === '@volter/world' && m.version) return m.version; } catch { /* not there */ }
820
- }
821
- return '0.0.0';
817
+ return currentVersion() ?? '0.0.0';
822
818
  }
823
819
 
824
820
  async function main(): Promise<void> {
package/src/mcp.ts CHANGED
@@ -31,22 +31,31 @@ function volter(args: string[], dir: string, timeoutSeconds = 600): Promise<Ran>
31
31
  }
32
32
 
33
33
  /** A command that keeps running (the World's view, a sign-in waiting for approval): started once, answered as soon as
34
- * its first lines say what the agent needs, left running for as long as this server runs. */
34
+ * its announcement says what the agent needs, left running for as long as this server runs. */
35
35
  const background = new Map<string, ChildProcess>();
36
- function startBackground(key: string, args: string[], dir: string, ready: RegExp, seconds = 60): Promise<Ran> {
36
+ function startBackground(key: string, args: string[], dir: string, ready: RegExp | ((output: string) => boolean), seconds = 60, incompleteCode = 0): Promise<Ran> {
37
37
  background.get(key)?.kill(); background.delete(key);
38
38
  return new Promise((done) => {
39
39
  const child = spawn(process.execPath, [CLI, ...args], { cwd: dir, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
40
40
  background.set(key, child);
41
41
  let output = ''; let answered = false;
42
42
  const answer = (code: number): void => { if (!answered) { answered = true; clearTimeout(timer); done({ code, output }); } };
43
- const take = (d: Buffer): void => { output += d.toString(); if (ready.test(output)) answer(0); };
43
+ const take = (d: Buffer): void => { output += d.toString(); if (typeof ready === 'function' ? ready(output) : ready.test(output)) answer(0); };
44
44
  child.stdout!.on('data', take); child.stderr!.on('data', take);
45
45
  child.on('error', (e) => { output += e.message; answer(1); });
46
- child.on('close', (code) => { background.delete(key); answer(code ?? 1); });
47
- const timer = setTimeout(() => answer(0), seconds * 1000);
46
+ child.on('close', (code) => { background.delete(key); answer(code === 0 ? incompleteCode : code ?? 1); });
47
+ const timer = setTimeout(() => answer(incompleteCode), seconds * 1000);
48
48
  });
49
49
  }
50
+
51
+ /** cli.ts finishes the access announcement after the runtime's initial viewing/console lines. */
52
+ function viewAnnouncementComplete(output: string): boolean {
53
+ if (/(?:^|\n)apps[ \t]+/.test(output)) {
54
+ // The local-origin announcement ends with its browser guidance, after any missing-console notice.
55
+ return /(?:^|\n)\(the World opens at its own \*\.localhost origin;[^\r\n]*\)\r?\n/.test(output);
56
+ }
57
+ return /(?:^|\n)(?:read-only view[ \t]+\S+|\(no console installed:[^\r\n]*\))\r?\n/.test(output);
58
+ }
50
59
  /** Everything this server left running, ended: a World under a dashboard is brought down through its own command first
51
60
  * (its state kept, its teardown recorded), then the command itself. */
52
61
  function stopAll(): void {
@@ -111,7 +120,7 @@ export async function serveMcp(version: string): Promise<void> {
111
120
  const root = here(dir);
112
121
  let before = '';
113
122
  try { const s = World.open({ root }).status(); if (s.running && !s.served) { const down = await volter(['world', 'down'], root); if (down.code !== 0) return ran(down); before = `${down.output.trim()} (state kept; the dashboard runs it now)\n`; } } catch (e) { return fail(e); }
114
- const r = await startBackground(`view:${root}`, ['world', 'view', '--no-open'], root, /console\s+\S+/);
123
+ const r = await startBackground(`view:${root}`, ['world', 'view', '--no-open'], root, viewAnnouncementComplete, 60, 1);
115
124
  return text(`${before}${r.output}`, r.code !== 0);
116
125
  });
117
126
 
@@ -11,9 +11,16 @@ const REGISTRY = 'https://registry.npmjs.org/@volter/world/latest';
11
11
 
12
12
  type Checked = { checkedAt: string; latest: string | null };
13
13
 
14
- /** This package's version, read from its own package.json. */
15
- export function currentVersion(): string {
16
- try { return (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version?: string }).version ?? '0.0.0'; } catch { return '0.0.0'; }
14
+ /** This package's version, read from its own package.json, or null when it cannot be established. */
15
+ export function currentVersion(): string | null {
16
+ // src/update-notice.ts in the repository, dist/src/update-notice.js in the published package
17
+ for (const up of ['../package.json', '../../package.json']) {
18
+ try {
19
+ const m = JSON.parse(readFileSync(new URL(up, import.meta.url), 'utf8')) as { name?: string; version?: string };
20
+ if (m.name === '@volter/world' && typeof m.version === 'string' && m.version) return m.version;
21
+ } catch { /* not there or unreadable */ }
22
+ }
23
+ return null;
17
24
  }
18
25
 
19
26
  /** Whether `a` is a newer release than `b` (major.minor.patch; a prerelease is not offered). */
@@ -28,6 +35,8 @@ export function newer(a: string, b: string): boolean {
28
35
  * done, waits a moment at most for it and prints the notice when there is one. Off, it does nothing and asks nothing. */
29
36
  export function startUpdateCheck(args: string[], env: NodeJS.ProcessEnv = process.env): () => Promise<void> {
30
37
  if (env.VOLTER_NO_UPDATE_NOTIFIER || env.CI || args.includes('--json') || !process.stderr.isTTY) return async () => undefined;
38
+ const here = currentVersion();
39
+ if (!here) return async () => undefined;
31
40
  const path = join(dirname(credentialsPath()), 'update-check.json');
32
41
  let kept: Checked | null = null;
33
42
  try { if (existsSync(path)) kept = JSON.parse(readFileSync(path, 'utf8')) as Checked; } catch { kept = null; }
@@ -38,7 +47,6 @@ export function startUpdateCheck(args: string[], env: NodeJS.ProcessEnv = proces
38
47
  : Promise.resolve(kept?.latest ?? null);
39
48
  return async () => {
40
49
  const latest = await Promise.race([check, new Promise<null>((r) => setTimeout(() => r(null), 800))]);
41
- const here = currentVersion();
42
50
  if (latest && newer(latest, here)) process.stderr.write(`\nvolter ${latest} is out (you have ${here}): npm install -g @volter/world\n`);
43
51
  };
44
52
  }