@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 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
- Create the workspace somewhere whose full path is made only of letters, digits, `-`,
365
- `_`, `.` and `/`. DuckDB cannot read a data file under a path containing a space, a
366
- parenthesis, an apostrophe, or any non-ASCII character, and Publisher resolves the
367
- model's relative table path against the workspace directory before that check runs. So
368
- a single space anywhere above the package makes every model in it fail to load, with
369
- the server still reporting healthy and the only visible symptom an empty package list.
370
-
371
- Common directories that trip it: `~/Documents/My Projects`, `~/Google Drive`,
372
- `~/OneDrive - Company`, and any home directory whose username carries an accent. The
373
- scaffolder checks this before writing anything and refuses to run in such a directory,
374
- naming the offending character. Move to a path like `~/malloy-workspace` and run again.
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.234";
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
- var DUCKDB_SAFE_PATH_CHAR = /[A-Za-z0-9._~:/?#@!$&*+,=%-]/;
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 ` + `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.`;
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 candidate = path3.sep === "\\" ? absolute.replace(/\\/g, "/") : absolute;
890
- for (const character of candidate) {
891
- if (!DUCKDB_SAFE_PATH_CHAR.test(character)) {
892
- throw new ScaffoldError(`This workspace cannot live in ${printable(absolute)}: DuckDB ` + `cannot read a ` + `data file under a path containing ${describeCharacter(character)}, so every model here would fail to load with the server ` + `still reporting healthy. Move to a path made of letters, ` + `digits, "-", "_", "." and "/" (for example ` + `~/malloy-workspace) and run again.`);
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
- " ": "a space",
899
- "'": "an apostrophe",
900
- '"': "a double quote",
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 codePoint < 32 || codePoint > 126 ? `${label} (U+${codePoint.toString(16).toUpperCase().padStart(4, "0")})` : label;
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 = `${startCommand}${result.hasStartScript ? " --" : ""} ` + `--port ${ALT_PUBLISHER_PORT} --mcp_port ${ALT_MCP_PORT}`;
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.5",
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.3",
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.426",
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",
@@ -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`. {{mcpNote}}
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