virtualmatter 0.1.0 → 0.2.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/README.md +48 -33
- package/dist/agentfiles.js +72 -0
- package/dist/api.js +93 -0
- package/dist/auth.js +10 -4
- package/dist/client.js +317 -0
- package/dist/config.js +23 -0
- package/dist/ignore.js +3 -1
- package/dist/index.js +189 -50
- package/dist/mcp.js +114 -22
- package/dist/pull.js +48 -41
- package/dist/resolve.js +65 -0
- package/dist/urls.js +80 -25
- package/package.json +4 -4
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn whatever the user handed us (a URL of any shape, a bare id, or
|
|
3
|
+
* nothing) into the one framing id every other command needs.
|
|
4
|
+
*/
|
|
5
|
+
import { getProject, getProjectByPublicId, listProjects } from "./api.js";
|
|
6
|
+
import { loadState } from "./state.js";
|
|
7
|
+
import { parseTarget } from "./urls.js";
|
|
8
|
+
/** The make framing of a project row, or a clear error when it has none. */
|
|
9
|
+
export function makeFramingOf(project) {
|
|
10
|
+
if (!project.make_framing_id) {
|
|
11
|
+
throw new Error(`Project "${project.name}" has no editing framing yet - open it in the browser once.`);
|
|
12
|
+
}
|
|
13
|
+
return project.make_framing_id;
|
|
14
|
+
}
|
|
15
|
+
export async function resolveParsedTarget(parsed, fetchFn = fetch) {
|
|
16
|
+
if (parsed.kind === "invalid")
|
|
17
|
+
throw new Error(parsed.reason);
|
|
18
|
+
if (parsed.kind === "framing")
|
|
19
|
+
return { framingId: parsed.framingId };
|
|
20
|
+
const project = parsed.publicId
|
|
21
|
+
? await getProjectByPublicId(parsed.publicId, fetchFn)
|
|
22
|
+
: await getProject(parsed.montageId, fetchFn);
|
|
23
|
+
return { framingId: makeFramingOf(project), project };
|
|
24
|
+
}
|
|
25
|
+
/** Resolve a pasted target string. */
|
|
26
|
+
export async function resolveTarget(target, fetchFn = fetch) {
|
|
27
|
+
return resolveParsedTarget(parseTarget(target), fetchFn);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* No target given: the folder's state file wins; otherwise, when the
|
|
31
|
+
* account has exactly one project, that one - nothing to choose between.
|
|
32
|
+
* Anything else throws with the list, so the caller can print it.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveImplicitTarget(dir, fetchFn = fetch) {
|
|
35
|
+
const state = loadState(dir);
|
|
36
|
+
if (state)
|
|
37
|
+
return { framingId: state.framing_id };
|
|
38
|
+
const projects = (await listProjects(fetchFn)).filter((p) => p.make_framing_id);
|
|
39
|
+
if (projects.length === 1)
|
|
40
|
+
return { framingId: projects[0].make_framing_id, project: projects[0] };
|
|
41
|
+
if (projects.length === 0) {
|
|
42
|
+
throw new Error('You have no projects yet. Create one with `npx virtualmatter create "My world"`.');
|
|
43
|
+
}
|
|
44
|
+
throw new NeedsChoiceError(projects);
|
|
45
|
+
}
|
|
46
|
+
export class NeedsChoiceError extends Error {
|
|
47
|
+
projects;
|
|
48
|
+
constructor(projects) {
|
|
49
|
+
super("Which project? Pass its URL or id. Yours:\n" + formatProjectList(projects));
|
|
50
|
+
this.projects = projects;
|
|
51
|
+
this.name = "NeedsChoiceError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function formatProjectList(projects) {
|
|
55
|
+
if (projects.length === 0)
|
|
56
|
+
return "(no projects yet)";
|
|
57
|
+
const width = Math.min(40, Math.max(...projects.map((p) => p.name.length)));
|
|
58
|
+
return projects
|
|
59
|
+
.map((p) => {
|
|
60
|
+
const name = p.name.length > width ? p.name.slice(0, width - 1) + "…" : p.name.padEnd(width);
|
|
61
|
+
const fid = p.make_framing_id ?? "(no editing framing)";
|
|
62
|
+
return ` ${name} ${fid} ${p.region}`;
|
|
63
|
+
})
|
|
64
|
+
.join("\n");
|
|
65
|
+
}
|
package/dist/urls.js
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parse
|
|
2
|
+
* Parse anything a user (or an agent) may paste and say what it names.
|
|
3
3
|
*
|
|
4
|
-
* Framing-shaped
|
|
5
|
-
* /edit/<framingId>
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Framing-shaped - the id is right there:
|
|
5
|
+
* /edit/<framingId> /edit/<slug>-<framingId>
|
|
6
|
+
* /play/<framingId> /m/<framingId>
|
|
7
|
+
* a bare framing id (21-char nanoid, or a legacy UUID)
|
|
8
|
+
* Project-shaped - resolved through the API to the project's make framing:
|
|
9
|
+
* /g/<montageUuid> /play/p/<montageUuid>
|
|
10
|
+
* /projects/<slug>-<publicId> /projects/<publicId> /projects/<montageUuid>
|
|
11
|
+
* /p/<slug>-<publicId> /p/<publicId>
|
|
12
|
+
* Accepted on any virtualmatter.ai / virtualmatter.dev host (plus localhost
|
|
13
|
+
* for dev), on the make.* and play.* subdomains alike.
|
|
14
|
+
*
|
|
15
|
+
* Readable segments are `<slug>-<identity>` with the identity FIXED-WIDTH and
|
|
16
|
+
* ANCHORED AT THE RIGHT END, mirroring `frontend/src/lib/readable-routes.ts`:
|
|
17
|
+
* framing ids are nanoids that routinely contain `-` (and can start with
|
|
18
|
+
* one), so splitting on any hyphen is wrong on real ids. Only `/edit/` is
|
|
19
|
+
* slug-aware; `/play/` and `/m/` are exact, so their segment is the whole id.
|
|
10
20
|
*/
|
|
11
21
|
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
22
|
+
/** Length of a minted `Framing.id` - mirrors `services/ids.py::FRAMING_ID_SIZE`. */
|
|
23
|
+
export const FRAMING_ID_LENGTH = 21;
|
|
24
|
+
/** Length of `Montage.public_id` - lowercase base36. */
|
|
25
|
+
export const PUBLIC_ID_LENGTH = 8;
|
|
26
|
+
const FRAMING_ID_CHARS_RE = /^[A-Za-z0-9_-]+$/;
|
|
27
|
+
const PUBLIC_ID_RE = /^[0-9a-z]{8}$/;
|
|
28
|
+
// Framing ids are nanoids (URL-safe alphabet, 21 chars as minted),
|
|
29
|
+
// case-SENSITIVE - never lowercase them. 15-32 keeps obvious typos and
|
|
30
|
+
// full URLs out while accepting every id shape the platform mints.
|
|
31
|
+
const NANOID_RE = /^[A-Za-z0-9_-]{15,32}$/;
|
|
12
32
|
function isVirtualmatterHost(host) {
|
|
13
33
|
const h = host.toLowerCase();
|
|
14
34
|
return (h === "virtualmatter.ai" ||
|
|
@@ -18,10 +38,34 @@ function isVirtualmatterHost(host) {
|
|
|
18
38
|
h === "localhost" ||
|
|
19
39
|
h === "127.0.0.1");
|
|
20
40
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
/** `<slug>-<identity>` with a fixed-width identity anchored at the right end. */
|
|
42
|
+
function splitFixedWidth(value, length) {
|
|
43
|
+
if (value.length < length + 2)
|
|
44
|
+
return null;
|
|
45
|
+
if (value[value.length - length - 1] !== "-")
|
|
46
|
+
return null;
|
|
47
|
+
return { slug: value.slice(0, value.length - length - 1), identity: value.slice(value.length - length) };
|
|
48
|
+
}
|
|
49
|
+
/** The framing id inside an `/edit/` segment: bare, or `<slug>-<id>`. */
|
|
50
|
+
export function framingIdFromEditSegment(segment) {
|
|
51
|
+
if (segment.length === FRAMING_ID_LENGTH)
|
|
52
|
+
return segment;
|
|
53
|
+
const split = splitFixedWidth(segment, FRAMING_ID_LENGTH);
|
|
54
|
+
if (split && FRAMING_ID_CHARS_RE.test(split.identity))
|
|
55
|
+
return split.identity;
|
|
56
|
+
return segment;
|
|
57
|
+
}
|
|
58
|
+
/** A `/p/` or `/projects/` segment as a project reference, or null. */
|
|
59
|
+
function projectFromReadableSegment(segment) {
|
|
60
|
+
if (UUID_RE.test(segment))
|
|
61
|
+
return { kind: "project", montageId: segment.toLowerCase() };
|
|
62
|
+
if (PUBLIC_ID_RE.test(segment))
|
|
63
|
+
return { kind: "project", publicId: segment };
|
|
64
|
+
const split = splitFixedWidth(segment, PUBLIC_ID_LENGTH);
|
|
65
|
+
if (split && PUBLIC_ID_RE.test(split.identity))
|
|
66
|
+
return { kind: "project", publicId: split.identity };
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
25
69
|
export function parseTarget(input) {
|
|
26
70
|
const trimmed = input.trim();
|
|
27
71
|
if (UUID_RE.test(trimmed)) {
|
|
@@ -41,22 +85,33 @@ export function parseTarget(input) {
|
|
|
41
85
|
return { kind: "invalid", reason: `Not a Virtual Matter host: ${url.hostname}` };
|
|
42
86
|
}
|
|
43
87
|
const parts = url.pathname.split("/").filter((p) => p.length > 0);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
const [first, second, third] = parts;
|
|
89
|
+
// Project-level shapes: /g/<uuid>, /play/p/<uuid>, /projects/<seg>, /p/<seg>
|
|
90
|
+
if (first === "g" && second) {
|
|
91
|
+
return projectFromReadableSegment(second) ?? { kind: "project", montageId: second };
|
|
92
|
+
}
|
|
93
|
+
if (first === "play" && second === "p" && third) {
|
|
94
|
+
return projectFromReadableSegment(third) ?? { kind: "project", montageId: third };
|
|
95
|
+
}
|
|
96
|
+
if ((first === "projects" || first === "p") && second) {
|
|
97
|
+
const project = projectFromReadableSegment(second);
|
|
98
|
+
if (project)
|
|
99
|
+
return project;
|
|
100
|
+
return { kind: "invalid", reason: `Not a readable project link: ${url.pathname}` };
|
|
101
|
+
}
|
|
102
|
+
// Framing shapes: /edit/<id or slug-id>, /play/<id>, /m/<id>
|
|
103
|
+
if (first === "edit" && second) {
|
|
104
|
+
const id = framingIdFromEditSegment(second);
|
|
105
|
+
return { kind: "framing", framingId: UUID_RE.test(id) ? id.toLowerCase() : id };
|
|
47
106
|
}
|
|
48
|
-
if (
|
|
49
|
-
return { kind: "
|
|
107
|
+
if ((first === "play" || first === "m") && second) {
|
|
108
|
+
return { kind: "framing", framingId: UUID_RE.test(second) ? second.toLowerCase() : second };
|
|
50
109
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
return { kind: "framing", framingId: id };
|
|
110
|
+
if (first === "new" || parts.length === 0) {
|
|
111
|
+
return {
|
|
112
|
+
kind: "invalid",
|
|
113
|
+
reason: "That link is the home page, not a world. Run `npx virtualmatter create \"My world\"` to make one, or `npx virtualmatter list` to see yours.",
|
|
114
|
+
};
|
|
58
115
|
}
|
|
59
116
|
return { kind: "invalid", reason: `Unrecognized Virtual Matter URL path: ${url.pathname}` };
|
|
60
117
|
}
|
|
61
|
-
/** Human guidance for project-shaped URLs (v1 keeps it simple). */
|
|
62
|
-
export const PROJECT_URL_HELP = "That link points at a whole project, not a single framing. Open the project in your browser and copy the /edit/<id> link for the framing you want, then try again.";
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "virtualmatter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI + MCP server for building with Virtual Matter - sync
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"virtualmatter": "
|
|
9
|
-
"vm": "
|
|
8
|
+
"virtualmatter": "dist/index.js",
|
|
9
|
+
"vm": "dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|