@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.
- package/dist/examples/customer-onboarding/README.md +19 -4
- package/dist/examples/customer-onboarding/package.json +6 -5
- package/dist/examples/customer-onboarding/server.mjs +1 -1
- package/dist/examples/integration-branches/README.md +4 -4
- package/dist/examples/integration-branches/package.json +6 -5
- package/dist/examples/workplace-agent/README.md +1 -1
- package/dist/examples/workplace-agent/package.json +6 -5
- package/dist/src/agents.d.ts +1 -1
- package/dist/src/agents.js +1 -3
- package/dist/src/cli.js +3 -12
- package/dist/src/mcp.js +14 -6
- package/dist/src/update-notice.d.ts +2 -2
- package/dist/src/update-notice.js +13 -7
- package/examples/customer-onboarding/README.md +19 -4
- package/examples/customer-onboarding/package.json +6 -5
- package/examples/customer-onboarding/server.mjs +1 -1
- package/examples/integration-branches/README.md +4 -4
- package/examples/integration-branches/package.json +6 -5
- package/examples/workplace-agent/README.md +1 -1
- package/examples/workplace-agent/package.json +6 -5
- package/package.json +5 -5
- package/src/agents.ts +1 -2
- package/src/cli.ts +2 -6
- package/src/mcp.ts +15 -6
- package/src/update-notice.ts +12 -4
|
@@ -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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
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.
|
|
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.
|
|
42
|
-
$ npx @volter/world@3.0.
|
|
43
|
-
$ npx @volter/world@3.0.
|
|
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.
|
|
13
|
-
"@volter/world-core": "3.0.
|
|
14
|
-
"@volter/world-runtime": "3.0.
|
|
15
|
-
"@volter/world-console": "3.0.
|
|
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.
|
|
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.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
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/dist/src/agents.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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[];
|
package/dist/src/agents.js
CHANGED
|
@@ -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
|
-
/**
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
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.
|
|
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.
|
|
42
|
-
$ npx @volter/world@3.0.
|
|
43
|
-
$ npx @volter/world@3.0.
|
|
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.
|
|
13
|
-
"@volter/world-core": "3.0.
|
|
14
|
-
"@volter/world-runtime": "3.0.
|
|
15
|
-
"@volter/world-console": "3.0.
|
|
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.
|
|
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.
|
|
15
|
-
"@volter/world-core": "3.0.
|
|
16
|
-
"@volter/world-runtime": "3.0.
|
|
17
|
-
"@volter/world-console": "3.0.
|
|
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.
|
|
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.
|
|
35
|
-
"@volter/world-runtime": "3.0.
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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
|
|
package/src/update-notice.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|