@malloy-publisher/skills 0.1.34 → 0.1.36
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 +27 -3
- package/dist/cli.d.ts +27 -0
- package/dist/cli.js +188 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.js +8 -9
- package/dist/install.d.ts +68 -0
- package/dist/install.js +182 -0
- package/dist/payload.d.ts +8 -0
- package/dist/payload.js +19 -0
- package/package.json +4 -1
- package/skills/eval-answer/SKILL.md +2 -3
- package/skills/eval-answer/reference/ledger-schema.md +8 -5
- package/skills/eval-answer/scripts/check_findable.py +121 -53
- package/skills/eval-answer/scripts/check_findable_test.py +153 -0
- package/skills/eval-answer/scripts/golden_rows.py +164 -0
- package/skills/eval-answer/scripts/golden_rows_test.py +178 -0
- package/skills/eval-answer/scripts/init_truth_package.py +7 -0
- package/skills/eval-answer/scripts/init_truth_package_test.py +20 -0
- package/skills/eval-answer/scripts/ledger.py +10 -5
- package/skills/eval-answer/scripts/ledger_test.py +11 -0
- package/skills/eval-answer/scripts/mcp_payload.py +13 -0
- package/skills/eval-answer/scripts/publisher_rest.py +15 -5
- package/skills/eval-answer/scripts/publisher_rest_test.py +20 -0
- package/skills/eval-answer/scripts/verify_goldens.py +39 -5
- package/skills/eval-answer/scripts/verify_goldens_test.py +102 -0
- package/skills/eval-diagnose/SKILL.md +2 -2
- package/skills/eval-diagnose/scripts/diagnose.py +16 -5
- package/skills/eval-diagnose/scripts/diagnose_test.py +19 -0
- package/skills/eval-import/SKILL.md +1 -1
- package/skills/eval-improve/SKILL.md +1 -1
- package/skills/eval-improve/scripts/improve.py +18 -4
- package/skills/eval-improve/scripts/improve_test.py +19 -0
- package/skills/eval-judge/SKILL.md +1 -1
- package/skills/eval-loop/SKILL.md +6 -4
- package/skills/eval-loop/reference/measurement.md +10 -6
- package/skills/eval-loop/reference/running-a-run.md +6 -6
- package/skills/eval-loop/scripts/agent_harness.py +1 -1
- package/skills/eval-loop/scripts/agent_harness_test.py +73 -17
- package/skills/eval-loop/scripts/build_run_package.py +21 -6
- package/skills/eval-loop/scripts/build_run_package_test.py +38 -1
- package/skills/eval-loop/scripts/check_judge.py +16 -3
- package/skills/eval-loop/scripts/check_judge_test.py +15 -0
- package/skills/eval-loop/scripts/flip_table.py +34 -6
- package/skills/eval-loop/scripts/flip_table_test.py +27 -1
- package/skills/eval-loop/scripts/run_baseline.py +326 -88
- package/skills/eval-loop/scripts/run_baseline_test.py +386 -1
- package/skills/eval-loop/scripts/serve.py +63 -40
- package/skills/eval-loop/scripts/serve_test.py +62 -4
- package/skills/eval-loop/templates/eval-run-package/eval_run.malloy +3 -0
- package/skills/eval-loop/templates/eval-run-package/public/app.js +2 -2
- package/skills/eval-report/SKILL.md +1 -1
- package/skills/malloy-analysis/SKILL.md +38 -34
- package/skills/malloy-analysis-report/SKILL.md +4 -4
- package/skills/malloy-charts/SKILL.md +33 -11
- package/skills/malloy-dashboards/SKILL.md +92 -122
- package/skills/malloy-define/SKILL.md +24 -6
- package/skills/malloy-discover/SKILL.md +23 -11
- package/skills/malloy-document/SKILL.md +2 -2
- package/skills/malloy-getting-started/SKILL.md +15 -2
- package/skills/malloy-gotchas-modeling/SKILL.md +34 -8
- package/skills/malloy-html-data-apps/SKILL.md +12 -12
- package/skills/{malloy-data-app-design → malloy-html-data-apps}/reference/archetypes.md +1 -1
- package/skills/{malloy-data-app-design → malloy-html-data-apps}/reference/depth-patterns.md +1 -1
- package/skills/{malloy-data-app-design/SKILL.md → malloy-html-data-apps/reference/design.md} +0 -5
- package/skills/{malloy-html-data-app-embedding/SKILL.md → malloy-html-data-apps/reference/embedding.md} +0 -5
- package/skills/{malloy-html-data-app-runtime/SKILL.md → malloy-html-data-apps/reference/runtime.md} +0 -5
- package/skills/malloy-html-data-apps/reference/verification-harness.md +2 -2
- package/skills/malloy-lookml-review/SKILL.md +1 -1
- package/skills/malloy-lookml-review/reference/propose-fields.md +1 -1
- package/skills/malloy-materialization/SKILL.md +4 -2
- package/skills/{malloy-materialization-tuning/SKILL.md → malloy-materialization/reference/tuning.md} +1 -7
- package/skills/malloy-model/SKILL.md +3 -4
- package/skills/malloy-model/reference/analysis-to-model.md +1 -1
- package/skills/malloy-model/reference/bridge-tables.md +13 -4
- package/skills/malloy-model-as-you-go/SKILL.md +5 -5
- package/skills/malloy-modeling/SKILL.md +14 -14
- package/skills/malloy-notebooks/SKILL.md +6 -4
- package/skills/malloy-phrase-detection/SKILL.md +2 -2
- package/skills/malloy-powerbi-review/SKILL.md +1 -1
- package/skills/malloy-publish/SKILL.md +5 -5
- package/skills/malloy-queries/SKILL.md +50 -4
- package/skills/malloy-review/SKILL.md +1 -1
- package/skills/malloy-review/reference/rubric-correctness.md +6 -6
- package/skills/malloy-review/reference/rubric-documentation.md +1 -1
- package/skills/malloy-review/reference/rubric-queries.md +3 -3
- package/skills/malloy-review/reference/rubric-rendering.md +6 -6
- package/skills/malloy-review/reference/severity-taxonomy.md +1 -1
- package/skills/malloy-source-unreachable/SKILL.md +4 -4
- package/skills/malloy/SKILL.md +0 -104
- package/skills/malloy-analysis-pitfalls/SKILL.md +0 -89
- package/skills/malloy-analyze/SKILL.md +0 -271
- package/skills/malloy-debug/SKILL.md +0 -129
- package/skills/malloy-gotchas-queries/SKILL.md +0 -211
- package/skills/malloy-gotchas-rendering/SKILL.md +0 -129
- package/skills/malloy-notebook-chat/SKILL.md +0 -25
- package/skills/malloy-patterns/SKILL.md +0 -122
- package/skills/malloy-scope/SKILL.md +0 -112
- /package/skills/{malloy-data-app-design → malloy-html-data-apps}/reference/token-contract.md +0 -0
- /package/skills/{malloy-data-app-design → malloy-html-data-apps}/reference/visual-craft.md +0 -0
package/README.md
CHANGED
|
@@ -5,6 +5,31 @@ SPDX-License-Identifier: MIT
|
|
|
5
5
|
|
|
6
6
|
# @malloy-publisher/skills
|
|
7
7
|
|
|
8
|
+
## Start here
|
|
9
|
+
|
|
10
|
+
1. **Learn.** Read the getting-started skill. Nothing to install, no server:
|
|
11
|
+
|
|
12
|
+
<https://unpkg.com/@malloy-publisher/skills@latest/skills/malloy-getting-started/SKILL.md>
|
|
13
|
+
|
|
14
|
+
Every skill reads the same way. Put its name in place of `malloy-getting-started`.
|
|
15
|
+
|
|
16
|
+
2. **Install.** If your agent has a shell, copy every skill to where it loads them:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx -y @malloy-publisher/skills@latest install # into this project
|
|
20
|
+
npx -y @malloy-publisher/skills@latest install --global # into your home directory
|
|
21
|
+
npx -y @malloy-publisher/skills@latest list # every skill, with a description
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Keep the `@latest`. Without it, npx may reuse an old cached copy and install old skills.
|
|
25
|
+
|
|
26
|
+
`install` picks the agent from what it finds. A `CLAUDE.md` means `.claude/skills/`. An
|
|
27
|
+
`AGENTS.md` or `.cursor/` means `.agents/skills/`. With `--global` it looks for `~/.claude/`,
|
|
28
|
+
`~/.agents/`, or `~/.cursor/` instead. If it finds none, it stops and asks you to name one:
|
|
29
|
+
`install claude` or `install agents`.
|
|
30
|
+
|
|
31
|
+
## What this package is
|
|
32
|
+
|
|
8
33
|
The agent skills that ship with [Malloy Publisher](https://github.com/malloydata/publisher), as
|
|
9
34
|
files you can install. They are task-specific guides for writing Malloy, building and reviewing
|
|
10
35
|
models, exploring data, and authoring HTML data apps.
|
|
@@ -14,7 +39,7 @@ files. This package exists for the cases MCP does not cover: a host that reads s
|
|
|
14
39
|
(Claude Code and the Publisher plugin), a scaffolder that installs them into a new project, or any
|
|
15
40
|
tool that wants the files themselves rather than prompt text.
|
|
16
41
|
|
|
17
|
-
##
|
|
42
|
+
## Use the files from code
|
|
18
43
|
|
|
19
44
|
```bash
|
|
20
45
|
npm install @malloy-publisher/skills
|
|
@@ -52,8 +77,7 @@ skill points to for detail it does not inline. Start with `malloy-getting-starte
|
|
|
52
77
|
`malloy-modeling` to build or change a model, `malloy-analysis` to explore and answer questions, and
|
|
53
78
|
`malloy-review` to check Malloy for correctness.
|
|
54
79
|
|
|
55
|
-
A few skills (`malloy-modeling`, `malloy-publish`, `malloy-document`, `malloy-getting-started
|
|
56
|
-
the `malloy` index) are written for a Publisher host and name Publisher's MCP tools
|
|
80
|
+
A few skills (`malloy-modeling`, `malloy-publish`, `malloy-document`, and `malloy-getting-started`) are written for a Publisher host and name Publisher's MCP tools
|
|
57
81
|
directly. The rest describe Malloy itself and refer to tools by bare name (`get_context`,
|
|
58
82
|
`execute_query`, `search_malloy_docs`), because the prefix depends on the
|
|
59
83
|
host. `search_database_schema` maps the same way if a shared skill starts using it. On Publisher those are `get_context`, `execute_query`, `search_malloy_docs`, and
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Where each host looks for skills, relative to a project or to the home dir. */
|
|
3
|
+
declare const TARGETS: Record<string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* What says a host is set up, per scope. The two differ because the hosts keep
|
|
6
|
+
* their user-level config in a directory rather than a top-level file: Claude
|
|
7
|
+
* Code reads `~/.claude/CLAUDE.md`, never `~/CLAUDE.md`, so the project markers
|
|
8
|
+
* would miss almost everyone who has Claude set up.
|
|
9
|
+
*/
|
|
10
|
+
declare const MARKERS: Record<"project" | "global", Record<string, string[]>>;
|
|
11
|
+
/**
|
|
12
|
+
* Where a skill can be read with nothing installed. `@latest` rather than this
|
|
13
|
+
* package's own version, so a link copied from an old cached copy of this CLI
|
|
14
|
+
* still serves the current skill.
|
|
15
|
+
*/
|
|
16
|
+
declare function skillUrl(name: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Which hosts this directory is set up for.
|
|
19
|
+
*
|
|
20
|
+
* Detection only ever chooses between hosts the user already has; it never
|
|
21
|
+
* decides *whether* to write. When it finds nothing, install asks rather than
|
|
22
|
+
* picking one, because a skills directory in the wrong place is invisible: the
|
|
23
|
+
* agent simply never loads them and nothing says why.
|
|
24
|
+
*/
|
|
25
|
+
declare function detectTargets(root: string, scope: keyof typeof MARKERS): string[];
|
|
26
|
+
declare function main(argv: string[]): void;
|
|
27
|
+
export { TARGETS, detectTargets, main, skillUrl };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) Credible Data Inc.
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
/**
|
|
5
|
+
* `malloy-skills` -- list the agent skills, and install them, with nothing
|
|
6
|
+
* running.
|
|
7
|
+
*
|
|
8
|
+
* This exists so an agent meeting Publisher for the first time can get the
|
|
9
|
+
* skills before it installs or starts anything. Every other route to them is
|
|
10
|
+
* gated: the MCP prompt channel needs the server up, and the server is a large
|
|
11
|
+
* install that clones its example packages over the network.
|
|
12
|
+
*
|
|
13
|
+
* Reading one skill needs no command at all: the package ships the files, so
|
|
14
|
+
* unpkg serves each SKILL.md at a stable URL (`skillUrl`). That works in a chat
|
|
15
|
+
* with no shell, which `npx` cannot. This CLI covers what a URL cannot: listing
|
|
16
|
+
* what ships, and copying the whole tree, `reference/` files included, onto
|
|
17
|
+
* disk through the hardened copy in install.ts.
|
|
18
|
+
*
|
|
19
|
+
* Which is why this lives in @malloy-publisher/skills and not in the server or a
|
|
20
|
+
* new package: this one already ships the skill files, has no runtime
|
|
21
|
+
* dependencies and no native code, so `npx` is fast and cannot fail on a build
|
|
22
|
+
* step or a platform binary. Adding a CLI framework here would cost that, for
|
|
23
|
+
* two commands, so the argument parsing below is by hand.
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from "node:fs";
|
|
26
|
+
import * as os from "node:os";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { listSkills } from "./index.js";
|
|
30
|
+
import { installSkills } from "./install.js";
|
|
31
|
+
import { skillsDir } from "./payload.js";
|
|
32
|
+
/** Where each host looks for skills, relative to a project or to the home dir. */
|
|
33
|
+
const TARGETS = {
|
|
34
|
+
claude: path.join(".claude", "skills"),
|
|
35
|
+
agents: path.join(".agents", "skills"),
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* What says a host is set up, per scope. The two differ because the hosts keep
|
|
39
|
+
* their user-level config in a directory rather than a top-level file: Claude
|
|
40
|
+
* Code reads `~/.claude/CLAUDE.md`, never `~/CLAUDE.md`, so the project markers
|
|
41
|
+
* would miss almost everyone who has Claude set up.
|
|
42
|
+
*/
|
|
43
|
+
const MARKERS = {
|
|
44
|
+
project: { claude: ["CLAUDE.md"], agents: ["AGENTS.md", ".cursor"] },
|
|
45
|
+
global: { claude: [".claude"], agents: [".agents", ".cursor"] },
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Where a skill can be read with nothing installed. `@latest` rather than this
|
|
49
|
+
* package's own version, so a link copied from an old cached copy of this CLI
|
|
50
|
+
* still serves the current skill.
|
|
51
|
+
*/
|
|
52
|
+
function skillUrl(name) {
|
|
53
|
+
return `https://unpkg.com/@malloy-publisher/skills@latest/skills/${name}/SKILL.md`;
|
|
54
|
+
}
|
|
55
|
+
const USAGE = `malloy-skills - list and install the Malloy Publisher agent skills
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
npx -y @malloy-publisher/skills@latest [list] List every skill.
|
|
59
|
+
npx -y @malloy-publisher/skills@latest install [host] Copy the skills onto disk.
|
|
60
|
+
|
|
61
|
+
Keep the @latest: without it npx may reuse an old cached copy.
|
|
62
|
+
|
|
63
|
+
Install targets:
|
|
64
|
+
claude .claude/skills/ (detected from a CLAUDE.md)
|
|
65
|
+
agents .agents/skills/ (detected from an AGENTS.md or .cursor/)
|
|
66
|
+
--global install into your home directory instead of this project
|
|
67
|
+
(detected from ~/.claude/, ~/.agents/, or ~/.cursor/)
|
|
68
|
+
|
|
69
|
+
Read one skill without installing anything:
|
|
70
|
+
${skillUrl("<name>")}
|
|
71
|
+
|
|
72
|
+
Nothing here needs a running server or an account.`;
|
|
73
|
+
function fail(message) {
|
|
74
|
+
console.error(message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
/** Print the name and description of every shipped skill. */
|
|
78
|
+
function list() {
|
|
79
|
+
const skills = listSkills();
|
|
80
|
+
if (skills.length === 0) {
|
|
81
|
+
fail(`No skills found in ${skillsDir}. This install of ` +
|
|
82
|
+
`@malloy-publisher/skills is incomplete; reinstall and try again.`);
|
|
83
|
+
}
|
|
84
|
+
for (const skill of skills) {
|
|
85
|
+
console.log(skill.name);
|
|
86
|
+
console.log(` ${skill.description}`);
|
|
87
|
+
}
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log(`Read one: ${skillUrl("<name>")}`);
|
|
90
|
+
console.log("Install them all: npx -y @malloy-publisher/skills@latest install");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Which hosts this directory is set up for.
|
|
94
|
+
*
|
|
95
|
+
* Detection only ever chooses between hosts the user already has; it never
|
|
96
|
+
* decides *whether* to write. When it finds nothing, install asks rather than
|
|
97
|
+
* picking one, because a skills directory in the wrong place is invisible: the
|
|
98
|
+
* agent simply never loads them and nothing says why.
|
|
99
|
+
*/
|
|
100
|
+
function detectTargets(root, scope) {
|
|
101
|
+
return Object.entries(MARKERS[scope])
|
|
102
|
+
.filter(([, markers]) => markers.some((marker) => fs.existsSync(path.join(root, marker))))
|
|
103
|
+
.map(([host]) => host);
|
|
104
|
+
}
|
|
105
|
+
/** Say what an install did, including what it cost. */
|
|
106
|
+
function report(target, dir, result) {
|
|
107
|
+
if (result.refused) {
|
|
108
|
+
fail(`Did not install into ${dir}: ${result.refused}.`);
|
|
109
|
+
}
|
|
110
|
+
console.log(`Installed ${result.installed} skills into ${dir}`);
|
|
111
|
+
if (result.skipped.length > 0) {
|
|
112
|
+
console.log(` left alone (symlinked): ${result.skipped.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
if (result.refreshed.length > 0) {
|
|
115
|
+
console.log(` replaced: ${result.refreshed.join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
// Not a footnote: these are files the user put there that no copy puts back.
|
|
118
|
+
for (const gone of result.removed) {
|
|
119
|
+
console.log(` removed (not in the shipped skill): ${target}/${gone}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function install(hosts, global) {
|
|
123
|
+
const root = global ? os.homedir() : process.cwd();
|
|
124
|
+
const scope = global ? "global" : "project";
|
|
125
|
+
let chosen = hosts;
|
|
126
|
+
if (chosen.length === 0) {
|
|
127
|
+
chosen = detectTargets(root, scope);
|
|
128
|
+
if (chosen.length === 0) {
|
|
129
|
+
const looked = Object.values(MARKERS[scope]).flat().join(", ");
|
|
130
|
+
const flag = global ? " --global" : "";
|
|
131
|
+
fail(`Could not tell which agent to install for: no ${looked} in ${root}.\n` +
|
|
132
|
+
`Name one: npx -y @malloy-publisher/skills@latest install claude${flag} (or: install agents${flag})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const host of chosen) {
|
|
136
|
+
const dir = path.join(root, TARGETS[host]);
|
|
137
|
+
report(host, dir, installSkills(dir, root));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function main(argv) {
|
|
141
|
+
const global = argv.includes("--global");
|
|
142
|
+
const args = argv.filter((arg) => arg !== "--global");
|
|
143
|
+
const [command = "list", ...rest] = args;
|
|
144
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
145
|
+
console.log(USAGE);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (command === "install") {
|
|
149
|
+
for (const host of rest) {
|
|
150
|
+
if (!(host in TARGETS)) {
|
|
151
|
+
fail(`Unknown install target '${host}'. ` +
|
|
152
|
+
`Expected one of: ${Object.keys(TARGETS).join(", ")}.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return install(rest, global);
|
|
156
|
+
}
|
|
157
|
+
// Anything else is refused rather than skipped: a silently ignored argument
|
|
158
|
+
// is how a typo turns into a command that looks like it worked.
|
|
159
|
+
if (command !== "list") {
|
|
160
|
+
// A skill name is the likeliest mistake, from an agent that expects this
|
|
161
|
+
// to print one, so it gets the URL that does.
|
|
162
|
+
const skill = listSkills().find((s) => s.name === command);
|
|
163
|
+
fail(skill
|
|
164
|
+
? `There is no command to print a skill. Read it at ${skillUrl(command)}`
|
|
165
|
+
: `Unknown command or option '${command}'.\n\n${USAGE}`);
|
|
166
|
+
}
|
|
167
|
+
if (rest.length > 0) {
|
|
168
|
+
fail(`'list' takes no arguments, got: ${rest.join(" ")}.`);
|
|
169
|
+
}
|
|
170
|
+
if (global)
|
|
171
|
+
fail("--global only applies to install.");
|
|
172
|
+
return list();
|
|
173
|
+
}
|
|
174
|
+
// Only when run as the command, so importing this module for a test does not
|
|
175
|
+
// parse the test runner's own argv and exit the process.
|
|
176
|
+
if (process.argv[1] &&
|
|
177
|
+
realpath(process.argv[1]) === realpath(fileURLToPath(import.meta.url))) {
|
|
178
|
+
main(process.argv.slice(2));
|
|
179
|
+
}
|
|
180
|
+
function realpath(target) {
|
|
181
|
+
try {
|
|
182
|
+
return fs.realpathSync(target);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export { TARGETS, detectTargets, main, skillUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,14 +5,6 @@ export interface Skill {
|
|
|
5
5
|
/** Absolute path to the skill's directory: SKILL.md and any reference/. */
|
|
6
6
|
dir: string;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Absolute path to the directory holding the skill directories.
|
|
10
|
-
*
|
|
11
|
-
* Resolved from this module's own URL, which works both from dist/ once
|
|
12
|
-
* installed and from src/ in the repo, since both sit one level under the
|
|
13
|
-
* package root.
|
|
14
|
-
*/
|
|
15
|
-
export declare const skillsDir: string;
|
|
16
8
|
/**
|
|
17
9
|
* Read every skill's frontmatter from the shipped files.
|
|
18
10
|
*
|
|
@@ -20,3 +12,10 @@ export declare const skillsDir: string;
|
|
|
20
12
|
* second copy of this metadata to regenerate or to drift.
|
|
21
13
|
*/
|
|
22
14
|
export declare function listSkills(): Skill[];
|
|
15
|
+
/**
|
|
16
|
+
* The installer is re-exported here so `@malloy-publisher/skills` has one entry
|
|
17
|
+
* point: consumers get `skillsDir` to read from and `installSkills` to write
|
|
18
|
+
* with, without reaching into a subpath.
|
|
19
|
+
*/
|
|
20
|
+
export { skillsDir } from "./payload.js";
|
|
21
|
+
export { countSkills, installSkills, isWithinDirectory, type SkillInstall, } from "./install.js";
|
package/dist/index.js
CHANGED
|
@@ -6,15 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
/**
|
|
11
|
-
* Absolute path to the directory holding the skill directories.
|
|
12
|
-
*
|
|
13
|
-
* Resolved from this module's own URL, which works both from dist/ once
|
|
14
|
-
* installed and from src/ in the repo, since both sit one level under the
|
|
15
|
-
* package root.
|
|
16
|
-
*/
|
|
17
|
-
export const skillsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
|
9
|
+
import { skillsDir } from "./payload.js";
|
|
18
10
|
function unquote(value) {
|
|
19
11
|
return value.trim().replace(/^["']|["']$/g, "");
|
|
20
12
|
}
|
|
@@ -47,3 +39,10 @@ export function listSkills() {
|
|
|
47
39
|
// platforms and must not depend on the runtime's locale.
|
|
48
40
|
return skills.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
49
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The installer is re-exported here so `@malloy-publisher/skills` has one entry
|
|
44
|
+
* point: consumers get `skillsDir` to read from and `installSkills` to write
|
|
45
|
+
* with, without reaching into a subpath.
|
|
46
|
+
*/
|
|
47
|
+
export { skillsDir } from "./payload.js";
|
|
48
|
+
export { countSkills, installSkills, isWithinDirectory, } from "./install.js";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface SkillInstall {
|
|
2
|
+
/** How many skills were copied in. */
|
|
3
|
+
installed: number;
|
|
4
|
+
/** Skill names left alone because the target is a symlink. */
|
|
5
|
+
skipped: string[];
|
|
6
|
+
/** Skill names whose existing files the bundled copies replaced. */
|
|
7
|
+
refreshed: string[];
|
|
8
|
+
/**
|
|
9
|
+
* Paths that were inside a skill directory, are not in the bundled copy of
|
|
10
|
+
* that skill, and so were removed and not written back. Relative to the
|
|
11
|
+
* skills directory (`malloy-review/NOTES.md`), directories with a trailing
|
|
12
|
+
* separator.
|
|
13
|
+
*
|
|
14
|
+
* Reported separately from `refreshed` because they are a different loss.
|
|
15
|
+
* "Refreshed" is read as "my edits to the skill files were reverted", which
|
|
16
|
+
* is recoverable knowledge; a file the user put in the directory themselves
|
|
17
|
+
* is simply gone, and nothing named it.
|
|
18
|
+
*/
|
|
19
|
+
removed: string[];
|
|
20
|
+
/** Why nothing at all was installed, when nothing was. */
|
|
21
|
+
refused?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Copy every agent skill into targetDir as real files. The generated project is
|
|
25
|
+
* standalone and the user may move or commit it, so this is a recursive copy, not
|
|
26
|
+
* a symlink: a symlink into node_modules would dangle the moment the project is
|
|
27
|
+
* moved or its dependencies pruned. skillsDir already excludes non-shipping skills
|
|
28
|
+
* and the internal README, so a straight copy is clean.
|
|
29
|
+
*
|
|
30
|
+
* A skill directory in the target that is itself a symlink is left alone. That is
|
|
31
|
+
* how this repo's own contributors wire skills into their agent (skills/README.md
|
|
32
|
+
* tells them to), and copying through the link would overwrite the source of truth
|
|
33
|
+
* rather than the workspace.
|
|
34
|
+
*
|
|
35
|
+
* targetDir has to resolve inside workspaceRoot. A `.claude` that is itself a
|
|
36
|
+
* symlink elsewhere is not caught by the per-skill check above, and used to write
|
|
37
|
+
* every skill outside the workspace while reporting them installed.
|
|
38
|
+
*
|
|
39
|
+
* An existing skill directory is removed before the copy rather than copied over.
|
|
40
|
+
* fs.cpSync follows a symlink it finds at the destination, so checking only the
|
|
41
|
+
* skill directory itself left every path inside it writable through a link: a
|
|
42
|
+
* `.claude/skills/malloy-review/SKILL.md` linked elsewhere had the file it points
|
|
43
|
+
* at replaced, and a linked `reference/` (four shipped skills have one) had files
|
|
44
|
+
* written into whatever directory it named, all under a green
|
|
45
|
+
* "Installed N Malloy skills". `.claude/skills/` is shared agent territory where
|
|
46
|
+
* links are an expected state, so the check has to cover the whole subtree, and
|
|
47
|
+
* removing first is the only form of that which cannot be one level short.
|
|
48
|
+
*
|
|
49
|
+
* The copy is unconditional, so an edited skill is replaced on every run. That is
|
|
50
|
+
* deliberate (an existing workspace picks up skill updates), but it is reported:
|
|
51
|
+
* `refreshed` names what was overwritten. Because the directory is removed and
|
|
52
|
+
* not merged, that goes further than reverting an edit: a file the user added
|
|
53
|
+
* inside a skill directory is gone, and no bundled copy puts it back. Those paths
|
|
54
|
+
* are collected into `removed` so the caller can name them, rather than leaving
|
|
55
|
+
* them under a word ("refreshed") that reads as "your edits were reverted". Note
|
|
56
|
+
* the copy never prunes whole skills, so one dropped from a later release stays in
|
|
57
|
+
* the workspace until deleted by hand.
|
|
58
|
+
*/
|
|
59
|
+
export declare function installSkills(targetDir: string, workspaceRoot: string): SkillInstall;
|
|
60
|
+
/** Count directories under dir that contain a SKILL.md, i.e. real skills. */
|
|
61
|
+
export declare function countSkills(dir: string): number;
|
|
62
|
+
/**
|
|
63
|
+
* Whether target, which need not exist yet, names a path inside root once every
|
|
64
|
+
* symlink on the way has been followed. A path utility rather than a skills
|
|
65
|
+
* concept, but it lives beside the only call that needs it, and exporting it
|
|
66
|
+
* keeps the scaffolder's own path checks on the same implementation.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isWithinDirectory(target: string, root: string): boolean;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Copyright (c) Credible Data Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { skillsDir } from "./payload.js";
|
|
6
|
+
/**
|
|
7
|
+
* Copy every agent skill into targetDir as real files. The generated project is
|
|
8
|
+
* standalone and the user may move or commit it, so this is a recursive copy, not
|
|
9
|
+
* a symlink: a symlink into node_modules would dangle the moment the project is
|
|
10
|
+
* moved or its dependencies pruned. skillsDir already excludes non-shipping skills
|
|
11
|
+
* and the internal README, so a straight copy is clean.
|
|
12
|
+
*
|
|
13
|
+
* A skill directory in the target that is itself a symlink is left alone. That is
|
|
14
|
+
* how this repo's own contributors wire skills into their agent (skills/README.md
|
|
15
|
+
* tells them to), and copying through the link would overwrite the source of truth
|
|
16
|
+
* rather than the workspace.
|
|
17
|
+
*
|
|
18
|
+
* targetDir has to resolve inside workspaceRoot. A `.claude` that is itself a
|
|
19
|
+
* symlink elsewhere is not caught by the per-skill check above, and used to write
|
|
20
|
+
* every skill outside the workspace while reporting them installed.
|
|
21
|
+
*
|
|
22
|
+
* An existing skill directory is removed before the copy rather than copied over.
|
|
23
|
+
* fs.cpSync follows a symlink it finds at the destination, so checking only the
|
|
24
|
+
* skill directory itself left every path inside it writable through a link: a
|
|
25
|
+
* `.claude/skills/malloy-review/SKILL.md` linked elsewhere had the file it points
|
|
26
|
+
* at replaced, and a linked `reference/` (four shipped skills have one) had files
|
|
27
|
+
* written into whatever directory it named, all under a green
|
|
28
|
+
* "Installed N Malloy skills". `.claude/skills/` is shared agent territory where
|
|
29
|
+
* links are an expected state, so the check has to cover the whole subtree, and
|
|
30
|
+
* removing first is the only form of that which cannot be one level short.
|
|
31
|
+
*
|
|
32
|
+
* The copy is unconditional, so an edited skill is replaced on every run. That is
|
|
33
|
+
* deliberate (an existing workspace picks up skill updates), but it is reported:
|
|
34
|
+
* `refreshed` names what was overwritten. Because the directory is removed and
|
|
35
|
+
* not merged, that goes further than reverting an edit: a file the user added
|
|
36
|
+
* inside a skill directory is gone, and no bundled copy puts it back. Those paths
|
|
37
|
+
* are collected into `removed` so the caller can name them, rather than leaving
|
|
38
|
+
* them under a word ("refreshed") that reads as "your edits were reverted". Note
|
|
39
|
+
* the copy never prunes whole skills, so one dropped from a later release stays in
|
|
40
|
+
* the workspace until deleted by hand.
|
|
41
|
+
*/
|
|
42
|
+
export function installSkills(targetDir, workspaceRoot) {
|
|
43
|
+
const empty = { installed: 0, skipped: [], refreshed: [], removed: [] };
|
|
44
|
+
if (isSymlink(targetDir)) {
|
|
45
|
+
return {
|
|
46
|
+
...empty,
|
|
47
|
+
refused: "it is a symlink, so writing into it would edit whatever it points at",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!isWithinDirectory(targetDir, workspaceRoot)) {
|
|
51
|
+
return {
|
|
52
|
+
...empty,
|
|
53
|
+
refused: "it resolves outside the workspace, so writing into it would put " +
|
|
54
|
+
"files somewhere else on this machine",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
58
|
+
const skipped = [];
|
|
59
|
+
const refreshed = [];
|
|
60
|
+
const removed = [];
|
|
61
|
+
let installed = 0;
|
|
62
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isDirectory()) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const source = path.join(skillsDir, entry.name);
|
|
67
|
+
if (!fs.existsSync(path.join(source, "SKILL.md"))) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const target = path.join(targetDir, entry.name);
|
|
71
|
+
if (isSymlink(target)) {
|
|
72
|
+
skipped.push(entry.name);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (fs.existsSync(target)) {
|
|
76
|
+
refreshed.push(entry.name);
|
|
77
|
+
// Read before the removal, because after it there is nothing to read:
|
|
78
|
+
// these are the paths the copy below does not put back.
|
|
79
|
+
for (const relative of pathsNotIn(target, source)) {
|
|
80
|
+
removed.push(path.join(entry.name, relative));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// target is not a symlink (checked above) and sits directly under a
|
|
84
|
+
// targetDir that resolves inside the workspace, so this removes files in
|
|
85
|
+
// the workspace and, for any symlink further down, the link itself rather
|
|
86
|
+
// than what it points at.
|
|
87
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
88
|
+
fs.cpSync(source, target, { recursive: true });
|
|
89
|
+
installed += 1;
|
|
90
|
+
}
|
|
91
|
+
return { installed, skipped, refreshed, removed };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every path under target that source does not also hold, relative to target.
|
|
95
|
+
*
|
|
96
|
+
* A directory that source does not hold is named once and not walked: what the
|
|
97
|
+
* user has to look for is the directory, and walking it would turn one dropped
|
|
98
|
+
* folder into a hundred lines. Nothing here follows a link out of target either,
|
|
99
|
+
* because a symlink is compared by its own path, never by what it points at.
|
|
100
|
+
*/
|
|
101
|
+
function pathsNotIn(target, source, prefix = "") {
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = fs.readdirSync(path.join(target, prefix), {
|
|
105
|
+
withFileTypes: true,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Unreadable: nothing can be said about what is in it.
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const missing = [];
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const relative = path.join(prefix, entry.name);
|
|
115
|
+
if (!fs.existsSync(path.join(source, relative))) {
|
|
116
|
+
missing.push(entry.isDirectory() ? relative + path.sep : relative);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
missing.push(...pathsNotIn(target, source, relative));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return missing;
|
|
124
|
+
}
|
|
125
|
+
/** Count directories under dir that contain a SKILL.md, i.e. real skills. */
|
|
126
|
+
export function countSkills(dir) {
|
|
127
|
+
if (!fs.existsSync(dir)) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
return fs
|
|
131
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
132
|
+
.filter((entry) => entry.isDirectory() &&
|
|
133
|
+
fs.existsSync(path.join(dir, entry.name, "SKILL.md"))).length;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether target, which need not exist yet, names a path inside root once every
|
|
137
|
+
* symlink on the way has been followed. A path utility rather than a skills
|
|
138
|
+
* concept, but it lives beside the only call that needs it, and exporting it
|
|
139
|
+
* keeps the scaffolder's own path checks on the same implementation.
|
|
140
|
+
*/
|
|
141
|
+
export function isWithinDirectory(target, root) {
|
|
142
|
+
const realRoot = realPathOf(root);
|
|
143
|
+
const realTarget = realPathOf(target);
|
|
144
|
+
return realTarget === realRoot || realTarget.startsWith(realRoot + path.sep);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The path target really names. fs.realpathSync cannot be used: it throws on a
|
|
148
|
+
* path that does not exist yet (every file we are about to create) and on a
|
|
149
|
+
* dangling symlink, which is precisely the case that matters, because
|
|
150
|
+
* fs.writeFileSync follows a dangling link and creates the file wherever it
|
|
151
|
+
* points. So resolve the parent first, then follow one link at a time.
|
|
152
|
+
*/
|
|
153
|
+
function realPathOf(target, hops = 0) {
|
|
154
|
+
const resolved = path.resolve(target);
|
|
155
|
+
const parent = path.dirname(resolved);
|
|
156
|
+
if (parent === resolved) {
|
|
157
|
+
return resolved;
|
|
158
|
+
}
|
|
159
|
+
const here = path.join(realPathOf(parent, hops), path.basename(resolved));
|
|
160
|
+
if (hops >= 40) {
|
|
161
|
+
// A symlink cycle. Stop following and judge the path as it stands.
|
|
162
|
+
return here;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
if (!fs.lstatSync(here).isSymbolicLink()) {
|
|
166
|
+
return here;
|
|
167
|
+
}
|
|
168
|
+
return realPathOf(path.resolve(path.dirname(here), fs.readlinkSync(here)), hops + 1);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Does not exist yet, or is unreadable: it is the path it looks like.
|
|
172
|
+
return here;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function isSymlink(target) {
|
|
176
|
+
try {
|
|
177
|
+
return fs.lstatSync(target).isSymbolicLink();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute path to the directory holding the skill directories.
|
|
3
|
+
*
|
|
4
|
+
* Resolved from this module's own URL, which works both from dist/ once
|
|
5
|
+
* installed and from src/ in the repo, since both sit one level under the
|
|
6
|
+
* package root.
|
|
7
|
+
*/
|
|
8
|
+
export declare const skillsDir: string;
|
package/dist/payload.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) Credible Data Inc.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Where the shipped skill files are.
|
|
5
|
+
*
|
|
6
|
+
* Its own module, rather than a constant in index.ts, because both the reader
|
|
7
|
+
* (`listSkills`) and the writer (`installSkills`) need it and index.ts re-exports
|
|
8
|
+
* the writer: importing it from there would make the two modules a cycle.
|
|
9
|
+
*/
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
/**
|
|
13
|
+
* Absolute path to the directory holding the skill directories.
|
|
14
|
+
*
|
|
15
|
+
* Resolved from this module's own URL, which works both from dist/ once
|
|
16
|
+
* installed and from src/ in the repo, since both sit one level under the
|
|
17
|
+
* package root.
|
|
18
|
+
*/
|
|
19
|
+
export const skillsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/skills",
|
|
3
3
|
"description": "Agent skills for Malloy and the Malloy Publisher",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.36",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"malloy-skills": "dist/cli.js"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
11
14
|
"types": "./dist/index.d.ts",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eval-answer
|
|
3
3
|
description: 'Score one analytical answer against a verified golden, and score which of the entities the golden depends on retrieval delivered to the answerer. Run the contamination checklist, re-execute the submitted query yourself, then spawn a judge subagent per skill:eval-judge. Append attempt, tool_call, score, and retrieval_score events to the file ledger (reference/ledger-schema.md). Never explain the failure (eval-diagnose) or edit the model (eval-improve). Use when asked whether an answer was correct, to score a run, or to baseline a model.'
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.36
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Evaluate One Answer
|
|
@@ -290,5 +290,4 @@ original answerer to "try again" with the new key in context.
|
|
|
290
290
|
|
|
291
291
|
- `skill:eval-diagnose`: why it failed, after this record exists.
|
|
292
292
|
- `skill:eval-improve`: smallest model edit, model-owned issues only.
|
|
293
|
-
-
|
|
294
|
-
The judge loads it by name; it is not part of the `eval` group.
|
|
293
|
+
- Step 5 of the `malloy-analysis` skill: checks before you trust a result you ran.
|