@malloy-publisher/create-malloy-package 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) The Linux Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,347 @@
1
+ # create-malloy-package
2
+
3
+ Scaffold a [Malloy Publisher](https://github.com/malloydata/publisher) package and a
4
+ local agent workspace in one command. You get a working semantic model over sample
5
+ data, a server config, and a pre-wired agent environment (MCP connection, agent
6
+ instructions, and the Malloy agent skills copied in), so you can go from nothing to
7
+ "an agent can query my data" without hand-editing anything.
8
+
9
+ ## Quick start
10
+
11
+ ```bash
12
+ mkdir my-data && cd my-data
13
+ npm create @malloy-publisher/malloy-package sales
14
+ npm start
15
+ ```
16
+
17
+ The workspace files land in the current directory and the package in `./sales`, so you
18
+ run `npm start` from where you created it (no need to `cd` into the package). The
19
+ scaffolder's own last lines are the ones to keep: the app URL, the MCP endpoint, and a
20
+ `curl` that tells you when the server is ready. `npm start` itself boots Publisher with
21
+ the package mounted in watch mode and then streams the server's log, which reports the
22
+ address it bound to (`http://127.0.0.1:4000`) and never says "ready" in so many words.
23
+ Run the readiness check the scaffolder printed:
24
+
25
+ ```bash
26
+ curl -s http://localhost:4000/api/v0/status
27
+ ```
28
+
29
+ It answers `"operationalState":"serving"` once the server is up. On a first run that
30
+ includes downloading the server, so it can take a minute; poll rather than guessing a
31
+ wait.
32
+
33
+ `serving` describes the server, not your package, so follow it with the question that
34
+ is actually about your data:
35
+
36
+ ```bash
37
+ curl -s http://localhost:4000/api/v0/environments/default/packages
38
+ ```
39
+
40
+ A package that failed to load is missing from that list, and an empty `[]` means
41
+ nothing loaded at all while the status endpoint still says `serving`. A data file
42
+ DuckDB cannot read is the usual cause; the server log carries the reason on an error
43
+ line reading `Failed to load package <name>`.
44
+
45
+ `default` is the environment name in a fresh workspace. If the scaffolder added your
46
+ package to a `publisher.config.json` that was already there, the environment is
47
+ whatever that file names, and the generated agent briefing spells out the URLs for
48
+ this workspace. That briefing is `AGENTS.md`, or `AGENTS.malloy.md` in a directory
49
+ that already had an `AGENTS.md` of its own; see "Running it again" below.
50
+
51
+ ## Query it
52
+
53
+ The web UI at http://localhost:4000 is the quickest look. For a check you can script,
54
+ every model is queryable over REST at
55
+ `POST /api/v0/environments/<env>/packages/<package>/models/<model>/query`. Run one of
56
+ the starter model's views by name:
57
+
58
+ ```bash
59
+ curl -s -X POST \
60
+ http://localhost:4000/api/v0/environments/default/packages/sales/models/sales.malloy/query \
61
+ -H 'Content-Type: application/json' \
62
+ -d '{"sourceName": "sales", "queryName": "by_category", "compactJson": true}' \
63
+ | jq -r .result
64
+ ```
65
+
66
+ ```json
67
+ [{"category":"Furniture","total_amount":789},{"category":"Electronics","total_amount":489.95},{"category":"Apparel","total_amount":375}]
68
+ ```
69
+
70
+ The source is named after the package, and the sample model ships the views
71
+ `by_category`, `by_region`, `sales_by_month` and `overview`. A package seeded with
72
+ `--data` starts with `overview` alone, since the scaffolder does not know your columns.
73
+ The model in the URL is the file inside the package. To send Malloy rather than name a
74
+ view, swap the `-d` payload for one that carries a query and no `sourceName`:
75
+
76
+ ```bash
77
+ -d '{"query": "run: sales -> by_category", "compactJson": true}'
78
+ ```
79
+
80
+ The response is `{"result": "<rows as a JSON string>", "resource": "..."}`, hence the
81
+ `jq -r .result` above. Leave `compactJson` off to get the full Malloy result with the
82
+ type metadata the renderer uses.
83
+
84
+ It has to be a POST, and the path has to be right: any `/api/v0/...` URL that matches
85
+ no route falls through to the web app and answers `200` with an HTML page, which reads
86
+ as success until you look at the body.
87
+
88
+ Agents should reach the same models through MCP rather than curl, which buys them
89
+ schema discovery, compile checks and a reload that needs no restart. The generated
90
+ briefing lists those tools.
91
+
92
+ ## What it creates
93
+
94
+ Running `npm create @malloy-publisher/malloy-package sales` in an empty directory produces:
95
+
96
+ ```
97
+ publisher.config.json the server config, with your package registered
98
+ package.json "npm start" runs Publisher in watch mode, "npm run reset"
99
+ does the same but rebuilds state from the config first
100
+ .gitignore ignores node_modules/, publisher_data/, publisher.db*,
101
+ *.log and .DS_Store
102
+ .mcp.json a pre-wired MCP connection to the local server
103
+ CLAUDE.md / AGENTS.md short, package-scoped agent instructions
104
+ .claude/skills/ the Malloy agent skills, copied in as real files
105
+ sales/ the package
106
+ publisher.json the manifest
107
+ sales.malloy a starter model over the sample data
108
+ data/sales.csv the sample data
109
+ ```
110
+
111
+ The `publisher.db*` entry is a glob rather than a plain `publisher.db` on purpose:
112
+ DuckDB leaves a `.wal` sidecar behind even after a clean shutdown, so without the glob
113
+ a `git add -A` would stage the server's database state.
114
+
115
+ ### Running it again
116
+
117
+ If the workspace files already exist (you ran it once before), a second run extends
118
+ them instead of overwriting them:
119
+
120
+ - the new package is added to the existing `publisher.config.json`, keeping the
121
+ environments, connections, and packages already registered there;
122
+ - the `malloy` server is merged into the existing MCP config (`.mcp.json`, or
123
+ `.cursor/mcp.json` for Cursor) alongside every other server in it, so a hand-edited
124
+ MCP config keeps the servers you put there;
125
+ - an existing `.gitignore` is appended to, never rewritten: it gains only the entries
126
+ above that are not in it already, keeps every rule of yours byte for byte, and is
127
+ listed under "Wrote these" as `.gitignore (n entries appended, nothing removed)`;
128
+ - the files it will not rewrite (`package.json`, `CLAUDE.md`, and an `AGENTS.md` this
129
+ tool did not write) are left as they are and listed under "Left these existing files
130
+ alone" in the output, so the run reports what it did not do as well as what it did.
131
+
132
+ The agent briefing is the exception to that last bullet, and has a section of its own
133
+ below.
134
+
135
+ `--force` replaces `CLAUDE.md`, and an `AGENTS.md` that is yours rather than this
136
+ tool's, outright, so move anything of yours out of them first. The other three files
137
+ are merges, not rewrites, because most of what is in them is yours:
138
+
139
+ - `.gitignore`: appended to, with or without `--force`. Appending is idempotent, so
140
+ `--force` changes nothing here and there is nothing of yours to move out first.
141
+ - `package.json`: `--force` sets the `start` and `reset` scripts and keeps the rest of
142
+ the manifest. Under `--force`, a `package.json` that cannot be read, or that is not
143
+ valid JSON, or that holds something other than a JSON object, stops the run before
144
+ anything is written at all, rather than being replaced with a stub. Without
145
+ `--force` the manifest is never written to in the first place, so a file in any of
146
+ those states is left alone and listed with the reason, and the run prints the full
147
+ `npx @malloy-publisher/server ...` command to use instead of an `npm` script.
148
+ - the MCP config (`.mcp.json`, or `.cursor/mcp.json` for Cursor): only the `malloy` key
149
+ is this tool's, so every other server in the file survives with or without `--force`.
150
+
151
+ An MCP config the tool cannot read as a JSON object is the one it cannot merge into: a
152
+ syntax error, or an `mcpServers` that is not an object. `--force` does not change that.
153
+ The file is left byte for byte as it was, listed under "Left these existing files
154
+ alone" with the reason, and the `malloy` server block is printed for you to paste in by
155
+ hand. Paste it, or fix the file and run again.
156
+
157
+ **Restart the second package with `npm run reset`, not `npm start`.** Registering the
158
+ package in `publisher.config.json` is not enough on its own once the server has run:
159
+ Publisher reads that file only while `publisher_data/` holds no database, and from the
160
+ second boot on its own persisted state wins. A package added later is therefore
161
+ registered, reported as created, and never served, with nothing in the server log to
162
+ say why. `npm run reset` is the same boot with `--init`, which rebuilds the persisted
163
+ state from the config; after it has served the new package once, `npm start` is enough
164
+ again. That holds for the `reset` script this tool writes. Where the scaffolder left an
165
+ existing `package.json` alone, a `reset` script already in it is not this tool's and
166
+ need not carry `--init` at all, so use the printed `npx @malloy-publisher/server ...`
167
+ command with `--init` on the end instead. `--init` is deliberately not part of `start`,
168
+ because it also clears runtime
169
+ state (materialized tables, saved theme) on every boot. If you moved the ports, pass
170
+ them here too (`npm run reset -- --port <p> --mcp_port <m>`); see "Ports and network
171
+ exposure" below.
172
+
173
+ **Stop the running server before you reset.** A workspace holds one DuckDB database and
174
+ one lock on it, so a second server over the same directory cannot start even on
175
+ different ports. It prints
176
+ `Could not set lock on file "publisher.db": Conflicting lock is held ... (PID nnnn)`,
177
+ then sits at `"operationalState":"initializing"` and stays there rather than exiting.
178
+ That covers `npm run reset` while `npm start` is up, and any second server pointed at
179
+ this same workspace. Two workspaces in two directories are fine, as long as they are
180
+ not on the same ports.
181
+
182
+ If the scaffolder left an existing `package.json` alone, it added no `start` or `reset`
183
+ script of its own. That manifest may well have scripts by those names already, but they
184
+ boot whatever their author meant them to boot: not necessarily Publisher, not
185
+ necessarily this workspace, not necessarily on loopback, and in the `reset` case not
186
+ necessarily with `--init`. So the run does not recommend them blind. It prints the full
187
+ `npx @malloy-publisher/server ...` command to run instead, and says that no Publisher
188
+ `start` (or `reset`) script was added here; the reset is that same command with `--init`
189
+ on the end. Where it read enough of the existing script to say something specific, it
190
+ gives the reason as well: that the script boots Publisher with no `--host`, or writes
191
+ the `--host=` form Publisher silently drops, or binds a non-loopback address, or names
192
+ the server inside a longer command line, or in the `reset` case carries no `--init`. A
193
+ script that boots something else entirely (a bundler, a test runner) gets the general
194
+ sentence rather than a named reason, because nothing here parsed what it runs.
195
+
196
+ #### The agent briefing, and `AGENTS.malloy.md`
197
+
198
+ The briefing is the one file that is regenerated rather than merged or skipped. It is
199
+ written in full from the run's own state (the start command, the ports, the package,
200
+ the skills count), so there is nothing in it to preserve, and a copy left describing
201
+ the workspace as it was two packages ago is the failure it exists to prevent.
202
+
203
+ - In a workspace this tool scaffolded, the briefing is `AGENTS.md`, and every later
204
+ run rewrites it. It is listed under "Overwrote these", alongside `.claude/skills/`,
205
+ which is refreshed the same way and for the same reason.
206
+ - In a directory that already had an `AGENTS.md` of its own, that file is not this
207
+ tool's to touch and is left byte for byte as it was. The briefing goes beside it as
208
+ **`AGENTS.malloy.md`**, and the `CLAUDE.md` the run writes points there instead, as
209
+ does the run's closing line. `AGENTS.malloy.md` is this tool's file, so later runs
210
+ rewrite it too.
211
+
212
+ Either way the briefing describes one package: the one that run created. Scaffold a
213
+ second package into the same workspace and the briefing is regenerated about the
214
+ second one, while the server goes on serving both. The list of everything the
215
+ workspace actually serves is the packages endpoint:
216
+
217
+ ```bash
218
+ curl -s http://localhost:4000/api/v0/environments/default/packages
219
+ ```
220
+
221
+ ## Ports and network exposure
222
+
223
+ The server listens on 4000 (web UI and REST) and 4040 (MCP), Publisher's defaults.
224
+ Pass `--port` and `--mcp_port` to move them, with npm's `--` separator in front:
225
+
226
+ ```bash
227
+ npm start -- --port 4100 --mcp_port 4140
228
+ ```
229
+
230
+ The flags belong to that one boot and nothing remembers them, so every later boot needs
231
+ them again, `npm run reset` included. A bare `npm run reset` goes back to 4000 and 4040
232
+ and collides with whatever was in the way the first time:
233
+
234
+ ```bash
235
+ npm run reset -- --port 4100 --mcp_port 4140
236
+ ```
237
+
238
+ 4100 and 4140 are a guess at a free pair, not a checked answer. Run the readiness check
239
+ against the new port and read `location` and `packages` out of it, exactly as you would
240
+ on 4000.
241
+
242
+ Moving the MCP port also means changing the `url` in `.mcp.json` (or
243
+ `.cursor/mcp.json`) to match, since that file was written for 4040.
244
+
245
+ **A port collision looks like success.** If another Publisher already holds 4000 and
246
+ 4040, the second one fails to bind and exits 1, but the last lines it prints are the
247
+ green `Service marked as ready` and `Environment store successfully initialized` from
248
+ the startup work that did finish; the `EADDRINUSE` error is well above them. The
249
+ readiness check then answers `"operationalState":"serving"` from the *other* server,
250
+ and because both workspaces name their environment `default`, the only tell is the
251
+ `location` and `packages` in the response describing a different directory. Check
252
+ those two fields, or move this server onto free ports.
253
+
254
+ Moving ports only helps when the server in the way is serving a different workspace. If
255
+ it is this workspace's own server, free ports change nothing: the second one takes the
256
+ database lock error above and never reaches `serving`. Stop the first server instead.
257
+
258
+ The `start` script this tool writes passes `--host 127.0.0.1`, so the workspace is
259
+ reachable from your machine only. Publisher's own default is `0.0.0.0`, which suits a
260
+ deployed server; here neither the REST API nor the MCP endpoint has any authentication
261
+ in front of it, so binding to every interface would hand the local network a read of
262
+ your data. To expose it deliberately, change `--host` in the `start` script to `0.0.0.0`
263
+ (or to a specific interface) and put something that authenticates in front of it.
264
+
265
+ All of that describes the script this tool writes. In a directory that already had a
266
+ `package.json`, the scaffolder leaves it alone (see "Running it again"), so `npm start`
267
+ runs whatever `start` was already there, binding wherever that command says. The same
268
+ goes for everything else on this page that reads off a flag: `--port` and `--mcp_port`
269
+ decide the URLs, `--server_root` and `--config` decide which workspace is served at
270
+ all, and `--watch-env` decides whether an edit on disk is picked up without a reload.
271
+ Read them out of the command you are about to run rather than off this page.
272
+
273
+ Then read it off the server, which is the only ground truth. On boot it prints
274
+ `Publisher server listening at http://127.0.0.1:4000` and an `MCP server listening at`
275
+ line beside it, and those are the addresses it really bound. Publisher accepts an
276
+ unknown flag without complaint, so a mistyped `--hostt` is dropped in silence and the
277
+ bind falls back to the `0.0.0.0` default while the command still reads as loopback. The
278
+ `=` form goes the same way: Publisher reads `--host` and its address as two separate
279
+ arguments, so `--host=127.0.0.1` matches no flag it knows, is dropped without a word,
280
+ and binds `0.0.0.0`. Write the flag and the address with a space between them.
281
+
282
+ ## Options
283
+
284
+ ```bash
285
+ npm create @malloy-publisher/malloy-package [name] -- [options]
286
+ ```
287
+
288
+ `npm create` parses the command line with npm's own config parser before handing
289
+ what is left to the scaffolder, so flags must sit behind a `--` separator. Without
290
+ it npm swallows `--force` as one of its own settings and turns `--data mydata.csv`
291
+ into two stray positional arguments:
292
+
293
+ ```bash
294
+ npm create @malloy-publisher/malloy-package sales -- --data mydata.csv
295
+ npm create @malloy-publisher/malloy-package sales -- --client cursor
296
+ npm create @malloy-publisher/malloy-package sales -- --force
297
+ ```
298
+
299
+ Running the published bin directly takes the flags as-is, with no separator:
300
+
301
+ ```bash
302
+ npx @malloy-publisher/create-malloy-package sales --data mydata.csv
303
+ ```
304
+
305
+ - `name` (positional): the package name. Omit it to only set up the agent workspace in
306
+ the current directory (write the MCP connection, agent instructions, and skills)
307
+ without scaffolding a package.
308
+ - `--data <file>`: seed the package from your own CSV, Parquet, or XLSX file instead of
309
+ the built-in sample. The file is copied into the package and the starter model points
310
+ at it. DuckDB reads all three formats in place; an Excel file is read as its first
311
+ sheet. It seeds a new package, so it requires a package name: it cannot be combined
312
+ with the setup-only mode above, and passing it without a name is an error rather than
313
+ a silently ignored flag.
314
+ - `--client <claude-code|cursor>`: which agent client to wire up. Defaults to
315
+ `claude-code`. `AGENTS.md` and the skills in `.claude/skills/` are written for every
316
+ client; the MCP config file (`.mcp.json` for Claude Code, `.cursor/mcp.json` for
317
+ Cursor) and `CLAUDE.md` are the client-specific parts. The flag is `--client` rather
318
+ than `--host` because the generated start command passes Publisher's own `--host`
319
+ for the bind address, and two meanings of one flag name in a single workspace is a
320
+ trap.
321
+ - `--force`: overwrite existing workspace files instead of extending them. The `--`
322
+ separator matters most here: npm has a `--force` of its own, so a bare one never
323
+ reaches the scaffolder.
324
+
325
+ ## Requirements
326
+
327
+ Node 20 or newer. The generated `npm start` uses `npx` to fetch the Publisher server on
328
+ first run, so no global install is needed. The default template runs entirely on the
329
+ package's built-in DuckDB sandbox, so no database credentials are required.
330
+
331
+ ### The workspace path
332
+
333
+ Create the workspace somewhere whose full path is made only of letters, digits, `-`,
334
+ `_`, `.` and `/`. DuckDB cannot read a data file under a path containing a space, a
335
+ parenthesis, an apostrophe, or any non-ASCII character, and Publisher resolves the
336
+ model's relative table path against the workspace directory before that check runs. So
337
+ a single space anywhere above the package makes every model in it fail to load, with
338
+ the server still reporting healthy and the only visible symptom an empty package list.
339
+
340
+ Common directories that trip it: `~/Documents/My Projects`, `~/Google Drive`,
341
+ `~/OneDrive - Company`, and any home directory whose username carries an accent. The
342
+ scaffolder checks this before writing anything and refuses to run in such a directory,
343
+ naming the offending character. Move to a path like `~/malloy-workspace` and run again.
344
+
345
+ ## License
346
+
347
+ MIT