@malloy-publisher/create-malloy-package 0.0.5 → 0.0.7
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 +30 -11
- package/dist/index.js +77 -17
- package/package.json +3 -3
- package/templates/AGENTS.md +3 -1
package/README.md
CHANGED
|
@@ -135,6 +135,10 @@ CLAUDE.md / AGENTS.md short, package-scoped agent instructions
|
|
|
135
135
|
.claude/skills/ the Malloy agent skills, copied in as real files
|
|
136
136
|
sales/ the package
|
|
137
137
|
publisher.json the manifest
|
|
138
|
+
malloy-config.json for the VS Code/Cursor Malloy extension, whose relative-path
|
|
139
|
+
resolution differs from Publisher's (machine-specific
|
|
140
|
+
absolute path; drop it and open the editor at sales/ instead
|
|
141
|
+
if you commit the package)
|
|
138
142
|
sales.malloy a starter model over the sample data
|
|
139
143
|
data/sales.csv the sample data
|
|
140
144
|
```
|
|
@@ -361,17 +365,32 @@ package's built-in DuckDB sandbox, so no database credentials are required.
|
|
|
361
365
|
|
|
362
366
|
### The workspace path
|
|
363
367
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
368
|
+
Spaces, apostrophes, and quotes in the workspace path are fine for querying data: the
|
|
369
|
+
model's data references are relative (`data/sales.csv`) and Publisher's per-package
|
|
370
|
+
DuckDB sandbox resolves them against the package's working directory, so the workspace
|
|
371
|
+
path never reaches DuckDB's path parser on that path. `~/Documents/My Projects`,
|
|
372
|
+
`~/Google Drive`, `~/OneDrive - Company`, and a `2026-08-13 Project Name` directory all
|
|
373
|
+
work — verified end-to-end (load and query a CSV) under paths containing a space, an
|
|
374
|
+
apostrophe, and a double quote. An earlier version of this tool refused all of them on a
|
|
375
|
+
premise that does not hold against the current server.
|
|
376
|
+
|
|
377
|
+
One server code path is an exception: the databases endpoint's schema probe builds an
|
|
378
|
+
absolute path literal and does reach the path parser, so a spaced/quoted workspace path
|
|
379
|
+
still loses row counts and column types there (the package still loads and queries
|
|
380
|
+
correctly). Pre-existing server behavior, tracked separately — not a reason to avoid
|
|
381
|
+
these paths.
|
|
382
|
+
|
|
383
|
+
What is refused is a path outside **printable ASCII** — an accent, an emoji, any
|
|
384
|
+
non-ASCII character, and every control character. This is the server's own rule, not
|
|
385
|
+
DuckDB's: Publisher checks an environment path against `[\x20-\x7E]` before mounting a
|
|
386
|
+
package from it, so a workspace under `~/josé` loads nothing. The server still reports
|
|
387
|
+
`serving`, with the environment missing and the reason only in the `loadErrors` of
|
|
388
|
+
`/api/v0/status`, so the scaffolder refuses up front instead — before anything is
|
|
389
|
+
written, naming the character. Control characters are refused for the additional reason
|
|
390
|
+
that they corrupt the commands and briefing files the scaffold writes verbatim.
|
|
391
|
+
|
|
392
|
+
So a home directory whose username carries an accent needs the workspace somewhere
|
|
393
|
+
else, such as `~/malloy-workspace`.
|
|
375
394
|
|
|
376
395
|
## License
|
|
377
396
|
|
package/dist/index.js
CHANGED
|
@@ -246,6 +246,27 @@ function toMalloyIdentifier(name) {
|
|
|
246
246
|
return identifier;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
// src/node_version.ts
|
|
250
|
+
var MIN_NODE_MAJOR = 20;
|
|
251
|
+
var REQUIRED_NODE_RANGE = `>=${MIN_NODE_MAJOR}`;
|
|
252
|
+
function nodeVersionWarning(versions) {
|
|
253
|
+
if (versions.bunVersion) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const match = /^v?(\d+)\./.exec(versions.nodeVersion.trim());
|
|
257
|
+
if (!match || Number(match[1]) >= MIN_NODE_MAJOR) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
return [
|
|
261
|
+
`! Node.js ${MIN_NODE_MAJOR} or newer is required to run Publisher. This shell is running Node.js ${versions.nodeVersion}.`,
|
|
262
|
+
` The workspace is scaffolded and correct, but Publisher will not work on this Node:`,
|
|
263
|
+
` upgrade before running npm start.`,
|
|
264
|
+
` nvm: nvm install ${MIN_NODE_MAJOR} && nvm use ${MIN_NODE_MAJOR}`,
|
|
265
|
+
` mise: mise use -g node@${MIN_NODE_MAJOR} then open a new shell`
|
|
266
|
+
].join(`
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
269
|
+
|
|
249
270
|
// src/registry_check.ts
|
|
250
271
|
import * as http from "node:http";
|
|
251
272
|
import * as https from "node:https";
|
|
@@ -644,14 +665,26 @@ var MCP_PORT = 4040;
|
|
|
644
665
|
var ALT_PUBLISHER_PORT = PUBLISHER_PORT + 100;
|
|
645
666
|
var ALT_MCP_PORT = MCP_PORT + 100;
|
|
646
667
|
var BIND_HOST = "127.0.0.1";
|
|
647
|
-
var SERVER_VERSION = "0.0.
|
|
668
|
+
var SERVER_VERSION = "0.0.244";
|
|
648
669
|
function startCommandFor(envName) {
|
|
649
670
|
return `npx -y @malloy-publisher/server@${SERVER_VERSION} --server_root . ` + `--config ./publisher.config.json --host ${BIND_HOST} ` + `--watch-env ${envName}`;
|
|
650
671
|
}
|
|
651
672
|
function resetCommandFor(envName) {
|
|
652
673
|
return `${startCommandFor(envName)} --init`;
|
|
653
674
|
}
|
|
654
|
-
|
|
675
|
+
function portOverrideCommandFor(result) {
|
|
676
|
+
return withAlternatePorts(result.hasStartScript ? "npm start" : result.startCommand, result.hasStartScript);
|
|
677
|
+
}
|
|
678
|
+
function resetPortOverrideCommandFor(result) {
|
|
679
|
+
return withAlternatePorts(result.hasResetScript ? "npm run reset" : result.resetCommand, result.hasResetScript);
|
|
680
|
+
}
|
|
681
|
+
function withAlternatePorts(command, viaNpmScript) {
|
|
682
|
+
return `${command}${viaNpmScript ? " --" : ""} ` + `--port ${ALT_PUBLISHER_PORT} --mcp_port ${ALT_MCP_PORT}`;
|
|
683
|
+
}
|
|
684
|
+
function isServablePathCharacter(character) {
|
|
685
|
+
const codePoint = character.codePointAt(0);
|
|
686
|
+
return codePoint >= 32 && codePoint <= 126;
|
|
687
|
+
}
|
|
655
688
|
var RESERVED_PACKAGE_NAMES = new Set([
|
|
656
689
|
"AGENTS.md",
|
|
657
690
|
MALLOY_AGENTS_FILE,
|
|
@@ -749,6 +782,15 @@ function createPackage(options, result) {
|
|
|
749
782
|
assertWithinWorkspace(dataDir, options.cwd, `${name}/data`);
|
|
750
783
|
fs4.mkdirSync(dataDir, { recursive: true });
|
|
751
784
|
writeFile(path3.join(packageDir, "publisher.json"), JSON.stringify({ name }, null, 2) + `
|
|
785
|
+
`, options.cwd);
|
|
786
|
+
writeFile(path3.join(packageDir, "malloy-config.json"), JSON.stringify({
|
|
787
|
+
connections: {
|
|
788
|
+
duckdb: {
|
|
789
|
+
is: "duckdb",
|
|
790
|
+
workingDirectory: path3.resolve(packageDir)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}, null, 2) + `
|
|
752
794
|
`, options.cwd);
|
|
753
795
|
let dataPath;
|
|
754
796
|
if (options.dataFile !== undefined) {
|
|
@@ -782,7 +824,7 @@ function createPackage(options, result) {
|
|
|
782
824
|
function forceDescription(name, modelFile, host) {
|
|
783
825
|
const agentFiles = host === "cursor" ? "AGENTS.md" : "AGENTS.md and CLAUDE.md";
|
|
784
826
|
const mcpPath = mcpConfigPathFor(host);
|
|
785
|
-
return `--force does not empty the directory. It rewrites ${name}/publisher.json, ` + `${name}/${modelFile} and the data file it copies into ${name}/data/, and
|
|
827
|
+
return `--force does not empty the directory. It rewrites ${name}/publisher.json, ` + `${name}/malloy-config.json, ${name}/${modelFile} and the data file it ` + `copies into ${name}/data/, and leaves anything else in there alone. ` + `It also refreshes .claude/skills/ ` + `from the bundled copies, as every run does. Outside the package it ` + `replaces ${agentFiles}; in package.json it sets only the "start" and ` + `"reset" scripts and keeps the rest of the file as it is; in ${mcpPath} it ` + `sets only the "malloy" server and keeps the others. publisher.config.json ` + `is extended, never rewritten, and .gitignore only gains the lines it is ` + `missing, with or without --force. None of those merges is a rewrite even ` + `when the file cannot be read: a package.json that is not valid JSON aborts ` + `the run before anything is written, and an unreadable ${mcpPath} or ` + `.gitignore is left alone and reported. If this directory has an AGENTS.md ` + `of its own, --force is not what you want for it: a run without --force ` + `keeps that file and writes the Publisher briefing to ${MALLOY_AGENTS_FILE} ` + `beside it instead, regenerating that one on every run.`;
|
|
786
828
|
}
|
|
787
829
|
function mcpConfigPathFor(host) {
|
|
788
830
|
return host === "cursor" ? ".cursor/mcp.json" : ".mcp.json";
|
|
@@ -886,24 +928,23 @@ function installWorkspaceSkills(cwd, result) {
|
|
|
886
928
|
}
|
|
887
929
|
function assertServablePath(cwd) {
|
|
888
930
|
const absolute = path3.resolve(cwd);
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
931
|
+
for (const character of absolute) {
|
|
932
|
+
if (!isServablePathCharacter(character)) {
|
|
933
|
+
const codePoint = character.codePointAt(0);
|
|
934
|
+
const consequence = codePoint < 32 || codePoint === 127 ? `which would corrupt the commands and briefing files this ` + `tool writes verbatim` : `and Publisher refuses to mount a package from a path ` + `outside printable ASCII: it would report serving with this ` + `workspace's environment missing, naming the reason only in ` + `/api/v0/status loadErrors`;
|
|
935
|
+
throw new ScaffoldError(`This workspace cannot live in ${printable(absolute)}: its path ` + `contains ${describeCharacter(character)}, ${consequence}. ` + `Move to a path of printable ASCII and run again — spaces are ` + `fine, so ~/Documents/My Projects works.`);
|
|
893
936
|
}
|
|
894
937
|
}
|
|
895
938
|
}
|
|
896
939
|
function describeCharacter(character) {
|
|
897
940
|
const named = {
|
|
898
|
-
"
|
|
899
|
-
"
|
|
900
|
-
|
|
901
|
-
"\\": "a backslash",
|
|
902
|
-
"\t": "a tab"
|
|
941
|
+
"\t": "a tab",
|
|
942
|
+
"\n": "a line feed",
|
|
943
|
+
"\r": "a carriage return"
|
|
903
944
|
};
|
|
904
|
-
const label = named[character] ?? `"${character}"`;
|
|
945
|
+
const label = named[character] ?? `"${printable(character)}"`;
|
|
905
946
|
const codePoint = character.codePointAt(0);
|
|
906
|
-
return
|
|
947
|
+
return `${label} (U+${codePoint.toString(16).toUpperCase().padStart(4, "0")})`;
|
|
907
948
|
}
|
|
908
949
|
function assertWorkspacePathsContained(options, mcpConfigPath) {
|
|
909
950
|
const relatives = [
|
|
@@ -1326,7 +1367,7 @@ function renderAgentsFile(result, host, envPackages) {
|
|
|
1326
1367
|
const skillsCount = String(result.skillsInstalled);
|
|
1327
1368
|
const skillsNote = result.skillsInstalled === 0 ? "This run installed no skills into `.claude/skills/` (the scaffolder's output says why), so there are none here to load. Pull the same guidance as MCP prompts from the endpoint above instead." : host === "cursor" ? `\`.claude/skills/\` holds ${skillsCount} Malloy agent skills as real files. Cursor reads \`AGENTS.md\`; these skills are the same guidance broken out by task, and any MCP client can also pull them as prompts from the endpoint above.` : `\`.claude/skills/\` holds ${skillsCount} Malloy agent skills as real files, so Claude Code auto-discovers them. Hosts that read \`AGENTS.md\` rather than Anthropic Agent Skills can pull the same guidance as MCP prompts from the endpoint above.`;
|
|
1328
1369
|
const startCommand = result.hasStartScript ? "npm start" : result.startCommand;
|
|
1329
|
-
const portOverrideCommand =
|
|
1370
|
+
const portOverrideCommand = portOverrideCommandFor(result);
|
|
1330
1371
|
return renderTemplate("AGENTS.md", {
|
|
1331
1372
|
title: result.packageCreated ? `${result.packageName}: a Malloy Publisher package` : "A Malloy Publisher workspace",
|
|
1332
1373
|
startCommand,
|
|
@@ -1357,7 +1398,7 @@ function packageSection(result, envPackages) {
|
|
|
1357
1398
|
];
|
|
1358
1399
|
if (result.packageCreated) {
|
|
1359
1400
|
const base = restBase(result.packageName);
|
|
1360
|
-
lines.push(`\`${result.packageName}/${result.modelFile}\` defines a Malloy source named \`${result.sourceName}\` over local data. Read it for the real source, field, and view names and use them verbatim; never guess them. The package's REST base is:`, "", "```", base, "```", "", "Run one of its views from a script:", "", "```bash", `curl -s -X POST ${base}/models/${result.modelFile}/query \\`, " -H 'content-type: application/json' \\", ` -d '{"query":"run: ${result.sourceName} -> overview"}'`, "```", "");
|
|
1401
|
+
lines.push(`\`${result.packageName}/${result.modelFile}\` defines a Malloy source named \`${result.sourceName}\` over local data. Read it for the real source, field, and view names and use them verbatim; never guess them. The package's REST base is:`, "", "```", base, "```", "", "Run one of its views from a script:", "", "```bash", `curl -s -X POST ${base}/models/${result.modelFile}/query \\`, " -H 'content-type: application/json' \\", ` -d '{"query":"run: ${result.sourceName} -> overview"}'`, "```", "", `The VS Code / Cursor Malloy extension resolves the model's relative \`duckdb.table('data/…')\` paths against the EDITOR WORKSPACE root, while Publisher resolves them against the package directory. \`${result.packageName}/malloy-config.json\` (generated, machine-specific absolute path) bridges that for an editor opened at this workspace root; without it, open the editor at \`${result.packageName}/\` itself, or a model Publisher serves fine shows unresolved-table errors in the editor.`, "");
|
|
1361
1402
|
}
|
|
1362
1403
|
if (others.length > 0) {
|
|
1363
1404
|
lines.push(otherPackagesParagraph(result, others, restBase), "");
|
|
@@ -1385,6 +1426,9 @@ function workspacePackageJson(cwd, result) {
|
|
|
1385
1426
|
name: toNpmName(path3.basename(path3.resolve(cwd))),
|
|
1386
1427
|
version: "0.1.0",
|
|
1387
1428
|
private: true,
|
|
1429
|
+
engines: {
|
|
1430
|
+
node: REQUIRED_NODE_RANGE
|
|
1431
|
+
},
|
|
1388
1432
|
scripts: {
|
|
1389
1433
|
start: result.startCommand,
|
|
1390
1434
|
reset: result.resetCommand
|
|
@@ -1399,7 +1443,8 @@ function workspaceGitignoreEntries() {
|
|
|
1399
1443
|
"publisher_data/",
|
|
1400
1444
|
"publisher.db*",
|
|
1401
1445
|
"*.log",
|
|
1402
|
-
".DS_Store"
|
|
1446
|
+
".DS_Store",
|
|
1447
|
+
"malloy-config.json"
|
|
1403
1448
|
];
|
|
1404
1449
|
}
|
|
1405
1450
|
function workspaceGitignore() {
|
|
@@ -1565,6 +1610,15 @@ async function run(name, options) {
|
|
|
1565
1610
|
if (warning) {
|
|
1566
1611
|
process.stdout.write(`
|
|
1567
1612
|
${yellow(warning)}
|
|
1613
|
+
`);
|
|
1614
|
+
}
|
|
1615
|
+
const nodeWarning = nodeVersionWarning({
|
|
1616
|
+
nodeVersion: process.version,
|
|
1617
|
+
bunVersion: process.versions.bun
|
|
1618
|
+
});
|
|
1619
|
+
if (nodeWarning) {
|
|
1620
|
+
process.stdout.write(`
|
|
1621
|
+
${yellow(nodeWarning)}
|
|
1568
1622
|
`);
|
|
1569
1623
|
}
|
|
1570
1624
|
} catch (err) {
|
|
@@ -1951,6 +2005,12 @@ function formatSuccess(result) {
|
|
|
1951
2005
|
if (!result.hasStartScript) {
|
|
1952
2006
|
lines.push(...exposureWarning("npm start", result.declinedStartScript));
|
|
1953
2007
|
}
|
|
2008
|
+
lines.push(dim(` If port ${result.publisherPort} is taken by a Publisher serving a ` + `DIFFERENT
|
|
2009
|
+
workspace, boot with
|
|
2010
|
+
` + ` ${result.needsReset ? resetPortOverrideCommandFor(result) : portOverrideCommandFor(result)}
|
|
2011
|
+
` + ` and edit the url in ${result.mcpConfigPath} to the new MCP port.` + (result.needsReset ? "" : `
|
|
2012
|
+
One already serving THIS workspace is the other case: ` + `stop it,
|
|
2013
|
+
because the lock is on the workspace, not the ` + `port.`)));
|
|
1954
2014
|
lines.push(` ${cyan(url)} ${dim("explore in the browser")}`);
|
|
1955
2015
|
lines.push("");
|
|
1956
2016
|
lines.push(bold("Check it is ready:"));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/create-malloy-package",
|
|
3
3
|
"description": "Scaffold a Malloy Publisher package and a local agent workspace, so one command takes you from nothing to an agent that can query your data.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"test:e2e": "bun test --timeout 180000 tests/e2e"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@malloy-publisher/skills": "^0.1.
|
|
48
|
+
"@malloy-publisher/skills": "^0.1.4",
|
|
49
49
|
"commander": "^12.1.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@eslint/js": "^8.57.0",
|
|
53
|
-
"@malloydata/malloy": "^0.0.
|
|
53
|
+
"@malloydata/malloy": "^0.0.427",
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
55
55
|
"@types/bun": "^1.2.21",
|
|
56
56
|
"@types/node": "^24.10.0",
|
package/templates/AGENTS.md
CHANGED
|
@@ -143,7 +143,9 @@ against the model, so it takes the snippet as a required `source` argument; it r
|
|
|
143
143
|
the model file's own errors too),
|
|
144
144
|
`malloy_reloadPackage` (pick up on-disk model edits with no restart, and surface a
|
|
145
145
|
watch-mode recompile that failed), and
|
|
146
|
-
`malloy_searchDocs
|
|
146
|
+
`malloy_searchDocs`, and `malloy_searchDatabaseSchema` (find the tables in a database
|
|
147
|
+
connection by plain-English description, for modelling data that is not in this package
|
|
148
|
+
yet; it returns each table's columns and the `source:` line to start from). {{mcpNote}}
|
|
147
149
|
|
|
148
150
|
REST, for a script or a check that does not need an agent: every model is queryable at
|
|
149
151
|
`POST /api/v0/environments/<env>/packages/<package>/models/<model>/query`, and after a
|