virtualmatter 0.2.0 → 0.4.0

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/mcp.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { uploadBatch } from "./agent-logs.js";
2
+ import { eventSchema } from "./log-events.js";
1
3
  /**
2
4
  * `virtualmatter mcp` - a stdio MCP server that gives a coding agent direct
3
5
  * hands on Virtual Matter: the account's projects, creating one, a live
@@ -12,7 +14,7 @@
12
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
16
  import { z } from "zod";
15
- import { createProject, defaultRegion, editUrl, listProjects, playUrl, resolveSession, sessionBaseUrl, } from "./api.js";
17
+ import { createProject, createWebsiteBuild, getWebsiteBuild, getEmbed, defaultRegion, editUrl, listProjects, playUrl, resolveSession, sessionBaseUrl, } from "./api.js";
16
18
  import { ensureClientInstalled, launchClient, seedNativeAuth } from "./client.js";
17
19
  import { packageVersion } from "./config.js";
18
20
  import { ConflictError, FilesClient } from "./files.js";
@@ -97,6 +99,23 @@ export async function writeWithRetry(client, filePath, body) {
97
99
  export async function runMcpServer(dir, framingFlag) {
98
100
  const ctx = makeContext(resolveFramingId(dir, framingFlag));
99
101
  const server = new McpServer({ name: "virtualmatter", version: packageVersion() });
102
+ server.registerTool("upload_agent_logs", {
103
+ description: "Append public conversation and tool events from any local harness to the selected project's Agent Logs. Supply stable event IDs for retry deduplication. Never include private reasoning, system prompts, credentials, or binary media. Use the CLI hook integrations for automatic full conversation capture.",
104
+ inputSchema: { harness: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/), session_id: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/), events: z.array(eventSchema).min(1).max(500) },
105
+ }, async ({ harness, session_id, events }) => {
106
+ const framing = ctx.requireFraming();
107
+ // Resolve afresh so a restarted session never strands retries on an old host.
108
+ const session = await resolveSession(framing);
109
+ return textResult(JSON.stringify(await uploadBatch(sessionBaseUrl(session), framing, harness, session_id, events)));
110
+ });
111
+ server.registerTool("get_embed", {
112
+ description: "Get canonical iframe markup and hosting instructions for a shared Virtual Matter world URL. No sign-in needed. Follow the instructions and verify the published page.",
113
+ inputSchema: { target: z.string() },
114
+ }, async ({ target }) => textResult(JSON.stringify(await getEmbed(target))));
115
+ server.registerTool("get_build_status", {
116
+ description: "Check a VM build by build_id. Do not create another project to check progress. Review the world and enable sharing before embedding.",
117
+ inputSchema: { build_id: z.string() },
118
+ }, async ({ build_id }) => textResult(JSON.stringify(await getWebsiteBuild(build_id))));
100
119
  server.registerTool("list_projects", {
101
120
  description: "List the signed-in maker's Virtual Matter projects (worlds): name, id, region, the editing framing id, and edit/play URLs. Call this to find a world to work in, then select_project.",
102
121
  inputSchema: {},
@@ -105,6 +124,8 @@ export async function runMcpServer(dir, framingFlag) {
105
124
  description: "Create a new Virtual Matter world (project) for the signed-in maker and select it for this session. The world spins up live on Virtual Matter servers; its content is SDK Lua plus assets that hot-reload as you write them. Region defaults to the one nearest this machine.",
106
125
  inputSchema: {
107
126
  name: z.string().min(1).max(100).describe("Project name, e.g. \"Lava Arena\""),
127
+ prompt: z.string().min(1).max(16000).optional().describe("Optional build prompt to send to VM's agent, using VM credits. Requires request_id. Starts privately."),
128
+ request_id: z.string().min(8).max(100).optional().describe("Stable id for a prompted build; reuse unchanged on retries."),
108
129
  description: z.string().max(1000).optional().describe("One-line description (optional)"),
109
130
  region: z.enum(["NA", "EU", "AS"]).optional().describe("Hosting region (default: nearest)"),
110
131
  pull_to: z
@@ -112,7 +133,17 @@ export async function runMcpServer(dir, framingFlag) {
112
133
  .optional()
113
134
  .describe("Local folder to mirror the new world's files into (optional; sync works from there)"),
114
135
  },
115
- }, async ({ name, description, region, pull_to }) => {
136
+ }, async ({ name, description, region, pull_to, prompt, request_id }) => {
137
+ if (prompt) {
138
+ if (!request_id)
139
+ throw new Error("A stable request_id is required when sending a build prompt.");
140
+ if (pull_to || description)
141
+ throw new Error("Prompted builds run in VM. Omit pull_to/description and use select_project afterwards if local files are needed.");
142
+ const build = await createWebsiteBuild({ name, prompt, request_id, region: region ?? defaultRegion() });
143
+ if (build.framing_id)
144
+ ctx.select(build.framing_id);
145
+ return textResult(JSON.stringify(build));
146
+ }
116
147
  const project = await createProject({ name, description, region: region ?? defaultRegion() });
117
148
  const framingId = makeFramingOf(project);
118
149
  ctx.select(framingId, project);
@@ -192,11 +223,30 @@ export async function runMcpServer(dir, framingFlag) {
192
223
  return textResult(JSON.stringify(await client.engineErrors(), null, 2));
193
224
  });
194
225
  server.registerTool("capture_screenshot", {
195
- description: "Capture a PNG screenshot of the engine's current view and return it as an image. Use it to visually verify what a change actually looks like in the world.",
196
- inputSchema: {},
197
- }, async () => {
226
+ description: "Capture a PNG screenshot of the world and return it as an image - how you SEE what you built. With no arguments it shoots a default overview of the world origin. Pass target to frame one object by name or id (easiest and usually what you want), or a full at + rot camera pose. Use it to visually verify a change instead of assuming it worked.",
227
+ inputSchema: {
228
+ target: z
229
+ .string()
230
+ .optional()
231
+ .describe("Object name or id to frame; the engine picks the distance"),
232
+ at: z
233
+ .array(z.number())
234
+ .length(3)
235
+ .optional()
236
+ .describe("Camera position [x, y, z]; Y is up and -Z is forward"),
237
+ rot: z
238
+ .array(z.number())
239
+ .length(3)
240
+ .optional()
241
+ .describe("Camera rotation in degrees as [yaw, pitch, roll] - that order. Yaw 0 faces -Z; pitch -90 looks straight down, 0 is the horizon. Default [0, -45, 0] looks down at the origin."),
242
+ },
243
+ }, async ({ target, at, rot }) => {
198
244
  const client = await ctx.getClient();
199
- const png = await client.screenshot();
245
+ const png = await client.screenshot({
246
+ target,
247
+ at: at,
248
+ rot: rot,
249
+ });
200
250
  return {
201
251
  content: [
202
252
  { type: "image", data: png.toString("base64"), mimeType: "image/png" },
package/dist/pull.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { resolveSession, sessionBaseUrl } from "./api.js";
5
- import { fetchAgentsMd, writeAgentFiles } from "./agentfiles.js";
5
+ import { composeAgentsMd, fetchAgentsMd, writeAgentFiles } from "./agentfiles.js";
6
6
  import { FilesClient } from "./files.js";
7
7
  import { isIgnoredPath } from "./ignore.js";
8
8
  import { saveState } from "./state.js";
@@ -33,6 +33,19 @@ function makeProgress(total) {
33
33
  },
34
34
  };
35
35
  }
36
+ /** The engine SDK's AGENTS.md from the Montage tree, or null when the world has none. */
37
+ export async function fetchEngineAgentsMd(client) {
38
+ try {
39
+ const listing = await client.list();
40
+ if (!listing.some((f) => f.path === "AGENTS.md"))
41
+ return null;
42
+ const { bytes } = await client.get("AGENTS.md");
43
+ return bytes.toString("utf8");
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
36
49
  export async function pullTree(client, dir, framingId) {
37
50
  fs.mkdirSync(dir, { recursive: true });
38
51
  const remote = (await client.list()).filter((f) => !isIgnoredPath(f.path));
@@ -64,6 +77,7 @@ export async function pullCommand(framingId, dir) {
64
77
  const client = new FilesClient(sessionBaseUrl(session), framingId);
65
78
  console.log(`Session ready at ${sessionBaseUrl(session)}. Downloading files ...`);
66
79
  const result = await pullTree(client, dir, framingId);
67
- result.agentFiles = writeAgentFiles(dir, await fetchAgentsMd());
80
+ const [platformDoc, engineDoc] = await Promise.all([fetchAgentsMd(), fetchEngineAgentsMd(client)]);
81
+ result.agentFiles = writeAgentFiles(dir, composeAgentsMd(platformDoc, engineDoc));
68
82
  return result;
69
83
  }
package/dist/sync.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { startLogSync } from "./agent-logs.js";
1
2
  /**
2
3
  * The sync hot loop: initial pull of remote changes, watch the local tree
3
4
  * and push edits with stored etags, poll the remote listing and pull files
@@ -213,6 +214,7 @@ export async function runSyncLoop(engine, dir) {
213
214
  const { default: chokidar } = await import("chokidar");
214
215
  const { pulled, pushed } = await engine.initialSync();
215
216
  console.log(`initial sync: ${pulled} pulled, ${pushed} pushed. Watching ${dir} ...`);
217
+ const stopLogs = startLogSync(dir);
216
218
  const watcher = chokidar.watch(dir, {
217
219
  ignoreInitial: true,
218
220
  ignored: (p) => {
@@ -241,6 +243,7 @@ export async function runSyncLoop(engine, dir) {
241
243
  const stop = async () => {
242
244
  clearInterval(interval);
243
245
  await watcher.close();
246
+ await stopLogs();
244
247
  console.log("\nsync stopped.");
245
248
  resolve();
246
249
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtualmatter",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI + MCP server for building with Virtual Matter - list and create worlds, sync their files, run Lua, capture screenshots, open the native client, and wire coding agents into a live session.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,6 +25,8 @@
25
25
  "@modelcontextprotocol/sdk": "^1.12.0",
26
26
  "chokidar": "^4.0.3",
27
27
  "commander": "^14.0.0",
28
+ "smol-toml": "^1.8.0",
29
+ "yaml": "^2.9.0",
28
30
  "zod": "^3.25.0"
29
31
  },
30
32
  "devDependencies": {