@malloy-publisher/create-malloy-package 0.0.6 → 0.0.8

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) The Linux Foundation
3
+ Copyright (c) Credible Data Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ <!--
2
+ Copyright (c) Credible Data Inc.
3
+ SPDX-License-Identifier: MIT
4
+ -->
5
+
1
6
  # create-malloy-package
2
7
 
3
8
  Scaffold a [Malloy Publisher](https://github.com/malloydata/publisher) package and a
@@ -135,6 +140,10 @@ CLAUDE.md / AGENTS.md short, package-scoped agent instructions
135
140
  .claude/skills/ the Malloy agent skills, copied in as real files
136
141
  sales/ the package
137
142
  publisher.json the manifest
143
+ malloy-config.json for the VS Code/Cursor Malloy extension, whose relative-path
144
+ resolution differs from Publisher's (machine-specific
145
+ absolute path; drop it and open the editor at sales/ instead
146
+ if you commit the package)
138
147
  sales.malloy a starter model over the sample data
139
148
  data/sales.csv the sample data
140
149
  ```
@@ -361,17 +370,32 @@ package's built-in DuckDB sandbox, so no database credentials are required.
361
370
 
362
371
  ### The workspace path
363
372
 
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.
373
+ Spaces, apostrophes, and quotes in the workspace path are fine for querying data: the
374
+ model's data references are relative (`data/sales.csv`) and Publisher's per-package
375
+ DuckDB sandbox resolves them against the package's working directory, so the workspace
376
+ path never reaches DuckDB's path parser on that path. `~/Documents/My Projects`,
377
+ `~/Google Drive`, `~/OneDrive - Company`, and a `2026-08-13 Project Name` directory all
378
+ work verified end-to-end (load and query a CSV) under paths containing a space, an
379
+ apostrophe, and a double quote. An earlier version of this tool refused all of them on a
380
+ premise that does not hold against the current server.
381
+
382
+ One server code path is an exception: the databases endpoint's schema probe builds an
383
+ absolute path literal and does reach the path parser, so a spaced/quoted workspace path
384
+ still loses row counts and column types there (the package still loads and queries
385
+ correctly). Pre-existing server behavior, tracked separately — not a reason to avoid
386
+ these paths.
387
+
388
+ What is refused is a path outside **printable ASCII** — an accent, an emoji, any
389
+ non-ASCII character, and every control character. This is the server's own rule, not
390
+ DuckDB's: Publisher checks an environment path against `[\x20-\x7E]` before mounting a
391
+ package from it, so a workspace under `~/josé` loads nothing. The server still reports
392
+ `serving`, with the environment missing and the reason only in the `loadErrors` of
393
+ `/api/v0/status`, so the scaffolder refuses up front instead — before anything is
394
+ written, naming the character. Control characters are refused for the additional reason
395
+ that they corrupt the commands and briefing files the scaffold writes verbatim.
396
+
397
+ So a home directory whose username carries an accent needs the workspace somewhere
398
+ else, such as `~/malloy-workspace`.
375
399
 
376
400
  ## License
377
401
 
package/dist/index.js CHANGED
@@ -665,14 +665,26 @@ var MCP_PORT = 4040;
665
665
  var ALT_PUBLISHER_PORT = PUBLISHER_PORT + 100;
666
666
  var ALT_MCP_PORT = MCP_PORT + 100;
667
667
  var BIND_HOST = "127.0.0.1";
668
- var SERVER_VERSION = "0.0.237";
668
+ var SERVER_VERSION = "0.2.0";
669
669
  function startCommandFor(envName) {
670
670
  return `npx -y @malloy-publisher/server@${SERVER_VERSION} --server_root . ` + `--config ./publisher.config.json --host ${BIND_HOST} ` + `--watch-env ${envName}`;
671
671
  }
672
672
  function resetCommandFor(envName) {
673
673
  return `${startCommandFor(envName)} --init`;
674
674
  }
675
- 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
+ }
676
688
  var RESERVED_PACKAGE_NAMES = new Set([
677
689
  "AGENTS.md",
678
690
  MALLOY_AGENTS_FILE,
@@ -770,6 +782,15 @@ function createPackage(options, result) {
770
782
  assertWithinWorkspace(dataDir, options.cwd, `${name}/data`);
771
783
  fs4.mkdirSync(dataDir, { recursive: true });
772
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) + `
773
794
  `, options.cwd);
774
795
  let dataPath;
775
796
  if (options.dataFile !== undefined) {
@@ -803,7 +824,7 @@ function createPackage(options, result) {
803
824
  function forceDescription(name, modelFile, host) {
804
825
  const agentFiles = host === "cursor" ? "AGENTS.md" : "AGENTS.md and CLAUDE.md";
805
826
  const mcpPath = mcpConfigPathFor(host);
806
- 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.`;
807
828
  }
808
829
  function mcpConfigPathFor(host) {
809
830
  return host === "cursor" ? ".cursor/mcp.json" : ".mcp.json";
@@ -907,24 +928,23 @@ function installWorkspaceSkills(cwd, result) {
907
928
  }
908
929
  function assertServablePath(cwd) {
909
930
  const absolute = path3.resolve(cwd);
910
- const candidate = path3.sep === "\\" ? absolute.replace(/\\/g, "/") : absolute;
911
- for (const character of candidate) {
912
- if (!DUCKDB_SAFE_PATH_CHAR.test(character)) {
913
- 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.`);
914
936
  }
915
937
  }
916
938
  }
917
939
  function describeCharacter(character) {
918
940
  const named = {
919
- " ": "a space",
920
- "'": "an apostrophe",
921
- '"': "a double quote",
922
- "\\": "a backslash",
923
- "\t": "a tab"
941
+ "\t": "a tab",
942
+ "\n": "a line feed",
943
+ "\r": "a carriage return"
924
944
  };
925
- const label = named[character] ?? `"${character}"`;
945
+ const label = named[character] ?? `"${printable(character)}"`;
926
946
  const codePoint = character.codePointAt(0);
927
- 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")})`;
928
948
  }
929
949
  function assertWorkspacePathsContained(options, mcpConfigPath) {
930
950
  const relatives = [
@@ -1347,7 +1367,7 @@ function renderAgentsFile(result, host, envPackages) {
1347
1367
  const skillsCount = String(result.skillsInstalled);
1348
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.`;
1349
1369
  const startCommand = result.hasStartScript ? "npm start" : result.startCommand;
1350
- const portOverrideCommand = `${startCommand}${result.hasStartScript ? " --" : ""} ` + `--port ${ALT_PUBLISHER_PORT} --mcp_port ${ALT_MCP_PORT}`;
1370
+ const portOverrideCommand = portOverrideCommandFor(result);
1351
1371
  return renderTemplate("AGENTS.md", {
1352
1372
  title: result.packageCreated ? `${result.packageName}: a Malloy Publisher package` : "A Malloy Publisher workspace",
1353
1373
  startCommand,
@@ -1378,7 +1398,7 @@ function packageSection(result, envPackages) {
1378
1398
  ];
1379
1399
  if (result.packageCreated) {
1380
1400
  const base = restBase(result.packageName);
1381
- 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.`, "");
1382
1402
  }
1383
1403
  if (others.length > 0) {
1384
1404
  lines.push(otherPackagesParagraph(result, others, restBase), "");
@@ -1423,7 +1443,8 @@ function workspaceGitignoreEntries() {
1423
1443
  "publisher_data/",
1424
1444
  "publisher.db*",
1425
1445
  "*.log",
1426
- ".DS_Store"
1446
+ ".DS_Store",
1447
+ "malloy-config.json"
1427
1448
  ];
1428
1449
  }
1429
1450
  function workspaceGitignore() {
@@ -1984,6 +2005,12 @@ function formatSuccess(result) {
1984
2005
  if (!result.hasStartScript) {
1985
2006
  lines.push(...exposureWarning("npm start", result.declinedStartScript));
1986
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.`)));
1987
2014
  lines.push(` ${cyan(url)} ${dim("explore in the browser")}`);
1988
2015
  lines.push("");
1989
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.6",
4
+ "version": "0.0.8",
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.4",
48
+ "@malloy-publisher/skills": "^0.1.9",
49
49
  "commander": "^12.1.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@eslint/js": "^8.57.0",
53
- "@malloydata/malloy": "^0.0.427",
53
+ "@malloydata/malloy": "^0.0.432",
54
54
  "@modelcontextprotocol/sdk": "^1.13.2",
55
55
  "@types/bun": "^1.2.21",
56
56
  "@types/node": "^24.10.0",
@@ -1,3 +1,8 @@
1
+ <!--
2
+ Copyright (c) Credible Data Inc.
3
+ SPDX-License-Identifier: MIT
4
+ -->
5
+
1
6
  # {{title}}
2
7
 
3
8
  This directory is a [Malloy Publisher](https://github.com/malloydata/publisher)
@@ -1,3 +1,6 @@
1
+ // Copyright (c) Credible Data Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
1
4
  // {{sourceName}}: a starter Malloy model over {{dataPath}}, served by Publisher.
2
5
  //
3
6
  // This model was scaffolded from a data file whose columns are not known ahead of
@@ -1,3 +1,6 @@
1
+ // Copyright (c) Credible Data Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
1
4
  // {{sourceName}}: a starter Malloy model over {{dataPath}}, served by Publisher.
2
5
  //
3
6
  // Check the row count before you trust this. A spreadsheet is only a table when
@@ -1,3 +1,6 @@
1
+ // Copyright (c) Credible Data Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
1
4
  // {{sourceName}}: a starter Malloy model over local CSV data, served by Publisher.
2
5
  //
3
6
  // The source reads data/sales.csv through the package's built-in DuckDB sandbox: