raga-cli 0.8.0 → 0.9.0

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -4
  3. package/bundled-skills/README.md +2 -2
  4. package/bundled-skills/bundle.json +73 -7
  5. package/bundled-skills/skills/bend/SKILL.md +58 -0
  6. package/bundled-skills/skills/bend/bend_tool.py +144 -0
  7. package/bundled-skills/skills/peak/SKILL.md +1 -1
  8. package/bundled-skills/skills/peak/references/raga-runtime/SKILL.md +18 -4
  9. package/bundled-skills/skills/peak/references/raga-runtime/raga_runtime.py +12 -1
  10. package/bundled-skills/skills/raga-artifacts/SKILL.md +117 -0
  11. package/bundled-skills/skills/raga-artifacts/artifact.mjs +254 -0
  12. package/bundled-skills/skills/raga-artifacts/artifact_tool.py +55 -0
  13. package/bundled-skills/skills/raga-artifacts/diagrams.md +641 -0
  14. package/bundled-skills/skills/raga-artifacts/templates/doc.html +124 -0
  15. package/bundled-skills/skills/raga-artifacts/templates/slides.html +138 -0
  16. package/dist/bundle/chunks/anthropic.js +1 -8
  17. package/dist/bundle/chunks/chunk-3MNXTBQ3.js +2 -0
  18. package/dist/bundle/chunks/{chunk-IOPE2TJ4.js → chunk-ENUUAC3O.js} +8 -8
  19. package/dist/bundle/chunks/{chunk-AI2NMHE7.js → chunk-F7LFZTHT.js} +261 -237
  20. package/dist/bundle/chunks/{chunk-3EGDWOKT.js → chunk-JQDZ4Z7L.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-J22MF7YW.js → chunk-NP6TH6UT.js} +2 -2
  22. package/dist/bundle/chunks/{chunk-YMZUNB25.js → chunk-OPPYO2IE.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-UDT647VB.js → chunk-S5J3UB5O.js} +1 -8
  24. package/dist/bundle/chunks/{cli-4P6FVVLT.js → cli-QLDCDYOR.js} +1 -1
  25. package/dist/bundle/chunks/{easter-egg-3d-65VIC4G2.js → easter-egg-3d-NPDBBPPF.js} +1 -1
  26. package/dist/bundle/chunks/{execute-3H5WKMA3.js → execute-WPTMS4XG.js} +1 -1
  27. package/dist/bundle/chunks/openai-chatgpt.js +1 -8
  28. package/dist/bundle/chunks/openai-codex.js +1 -8
  29. package/dist/bundle/chunks/openrouter.js +1 -8
  30. package/dist/bundle/chunks/radius.js +1 -8
  31. package/dist/bundle/chunks/{runtime-435R6IKY.js → runtime-N5MUH2WA.js} +1 -1
  32. package/dist/bundle/chunks/{virtual-modules-U3JBUDP7.js → virtual-modules-4I6Y6DPQ.js} +1 -1
  33. package/dist/bundle/cli-runtime.js +1 -1
  34. package/dist/bundle/index.js +1 -1
  35. package/dist/bundle/raga-runtime.js +1 -1
  36. package/dist/bundle/rpc-entry.js +1 -1
  37. package/docs/cli.md +6 -0
  38. package/docs/compaction.md +26 -0
  39. package/docs/sdk.md +2 -1
  40. package/docs/settings.md +1 -0
  41. package/package.json +1 -1
  42. package/python/raga_rlm/__init__.py +20 -4
  43. package/python/raga_rlm/mlx_models.py +265 -0
  44. package/schemas/keybindings.schema.json +48 -0
  45. package/schemas/settings.schema.json +14 -0
  46. package/dist/bundle/chunks/chunk-C4IJNE7U.js +0 -2
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build a self-contained artifact from an HTML source: render Mermaid blocks to SVG with Pele,
4
+ * highlight code, inline local images, check the layout, then print to PDF with headless Chrome.
5
+ *
6
+ * node artifact.mjs build <source.html> [--out <dir>] [--no-pdf] [--preview]
7
+ *
8
+ * Prints one JSON object. Exit 1 lists every problem instead of producing a partial artifact.
9
+ */
10
+ import { spawn } from "node:child_process";
11
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
12
+ import { createRequire } from "node:module";
13
+ import { tmpdir } from "node:os";
14
+ import { basename, delimiter, dirname, extname, join, resolve } from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+
17
+ /**
18
+ * Pele and highlight.js are pinned dependencies of raga. Resolve them from this file first, which works
19
+ * for the bundled copy inside the raga package, then from the installed raga executable, which works
20
+ * for a copy in an instance revision.
21
+ */
22
+ function packageRequires() {
23
+ const roots = [import.meta.url];
24
+ if (process.env.RAGA_PACKAGE_DIR) roots.push(pathToFileURL(join(process.env.RAGA_PACKAGE_DIR, "package.json")).href);
25
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
26
+ const raga = join(directory, "raga");
27
+ if (!existsSync(raga)) continue;
28
+ roots.push(pathToFileURL(realpathSync(raga)).href);
29
+ }
30
+ return roots.map((root) => createRequire(root));
31
+ }
32
+
33
+ function resolveDependency(name) {
34
+ for (const require of packageRequires()) {
35
+ try {
36
+ return require.resolve(name);
37
+ } catch {}
38
+ }
39
+ throw new Error(`Cannot find ${name}. Run the skill from a raga installation, or set RAGA_PACKAGE_DIR to the raga package directory.`);
40
+ }
41
+
42
+ const MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp" };
43
+ const ENTITIES = { "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'", "&apos;": "'", "&nbsp;": " ", "&amp;": "&" };
44
+ const decode = (text) => text.replace(/&(?:lt|gt|quot|#39|apos|nbsp|amp);/g, (entity) => ENTITIES[entity]);
45
+
46
+ function chromePath() {
47
+ const candidates = [
48
+ process.env.CHROME_PATH,
49
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
50
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
51
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
52
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
53
+ ];
54
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
55
+ for (const name of ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge"]) {
56
+ candidates.push(join(directory, name));
57
+ }
58
+ }
59
+ return candidates.find((path) => path && existsSync(path));
60
+ }
61
+
62
+ /** Run headless Chrome on a local file with a throwaway profile, so it never touches the user's browser. */
63
+ function runChrome(chrome, htmlPath, outputPath, extraArgs) {
64
+ return chromeProcess(chrome, htmlPath, extraArgs).then(() => {
65
+ if (!existsSync(outputPath)) throw new Error(`Chrome did not write ${outputPath}`);
66
+ });
67
+ }
68
+
69
+ /** Run headless Chrome and return its standard output. */
70
+ function runChromeOutput(chrome, htmlPath, extraArgs) {
71
+ return chromeProcess(chrome, htmlPath, extraArgs);
72
+ }
73
+
74
+ function chromeProcess(chrome, htmlPath, extraArgs) {
75
+ const profile = mkdtempSync(join(tmpdir(), "raga-artifact-chrome-"));
76
+ const args = [
77
+ "--headless=new",
78
+ "--disable-gpu",
79
+ "--no-first-run",
80
+ "--no-default-browser-check",
81
+ "--disable-extensions",
82
+ `--user-data-dir=${profile}`,
83
+ "--virtual-time-budget=5000",
84
+ ...extraArgs,
85
+ pathToFileURL(htmlPath).href,
86
+ ];
87
+ const { promise, resolve: done, reject } = Promise.withResolvers();
88
+ const child = spawn(chrome, args, { stdio: ["ignore", "pipe", "pipe"] });
89
+ let stdout = "";
90
+ let stderr = "";
91
+ child.stdout.on("data", (chunk) => {
92
+ if (stdout.length < 20_000_000) stdout += chunk;
93
+ });
94
+ child.stderr.on("data", (chunk) => {
95
+ if (stderr.length < 20000) stderr += chunk;
96
+ });
97
+ const timer = setTimeout(() => child.kill("SIGKILL"), 120000);
98
+ child.once("error", reject);
99
+ child.once("close", (code) => {
100
+ clearTimeout(timer);
101
+ rmSync(profile, { recursive: true, force: true });
102
+ if (code === 0) done(stdout);
103
+ else reject(new Error(`Chrome failed (exit ${code}). ${stderr.trim().slice(-2000)}`));
104
+ });
105
+ return promise;
106
+ }
107
+
108
+ /** Measure layout in Chrome: a slide whose content leaves its frame, or a wide block in a document. */
109
+ async function checkLayout(chrome, htmlPath) {
110
+ const probe = `<script>addEventListener("load", () => {
111
+ const issues = [];
112
+ document.querySelectorAll("section.slide").forEach((slide, index) => {
113
+ const frame = slide.getBoundingClientRect();
114
+ let bottom = 0, right = 0;
115
+ for (const el of slide.querySelectorAll("*")) {
116
+ if (el.closest(".footer")) continue;
117
+ const rect = el.getBoundingClientRect();
118
+ if (!rect.width || !rect.height) continue;
119
+ bottom = Math.max(bottom, rect.bottom - frame.top);
120
+ right = Math.max(right, rect.right - frame.left);
121
+ }
122
+ const over = Math.round(Math.max(bottom - frame.height, right - frame.width, slide.scrollHeight - slide.clientHeight));
123
+ if (over > 1) issues.push("Slide " + (index + 1) + " overflows its frame by " + over + "px. Cut content or split the slide.");
124
+ });
125
+ document.querySelectorAll("main pre, main table, main figure, main img").forEach((el) => {
126
+ const over = el.scrollWidth - el.clientWidth;
127
+ if (over > 1) issues.push("A " + el.tagName.toLowerCase() + " is " + over + "px wider than the page. Shorten lines or narrow it.");
128
+ });
129
+ const out = document.createElement("pre");
130
+ out.id = "raga-layout";
131
+ out.textContent = JSON.stringify(issues);
132
+ document.body.append(out);
133
+ });</script>`;
134
+ const directory = mkdtempSync(join(tmpdir(), "raga-artifact-layout-"));
135
+ try {
136
+ const probePath = join(directory, "probe.html");
137
+ writeFileSync(probePath, readFileSync(htmlPath, "utf8").replace(/<\/body>/i, `${probe}</body>`));
138
+ const dom = await runChromeOutput(chrome, probePath, ["--window-size=1680,1000", "--dump-dom"]);
139
+ const match = /<pre id="raga-layout">([\s\S]*?)<\/pre>/.exec(dom);
140
+ return match ? JSON.parse(decode(match[1])) : ["The layout check did not finish."];
141
+ } finally {
142
+ rmSync(directory, { recursive: true, force: true });
143
+ }
144
+ }
145
+
146
+ /** Page count from the PDF's page tree. Chrome writes it uncompressed. */
147
+ function pdfPages(pdfPath) {
148
+ const text = readFileSync(pdfPath).toString("latin1");
149
+ const counts = [...text.matchAll(/\/Type\s*\/Pages\b[^>]*?\/Count\s+(\d+)/g)].map((match) => Number(match[1]));
150
+ return counts.length ? Math.max(...counts) : undefined;
151
+ }
152
+
153
+ async function build(sourcePath, options) {
154
+ const source = readFileSync(sourcePath, "utf8");
155
+ const directory = dirname(sourcePath);
156
+ const problems = [];
157
+ const warnings = [];
158
+ if (/<script\b/i.test(source)) problems.push("Remove <script> elements. Artifacts are static so they print and share safely.");
159
+ if (/<(?:iframe|object|embed)\b/i.test(source)) problems.push("Remove <iframe>, <object>, and <embed> elements.");
160
+ if (/<link\b[^>]*href\s*=\s*["']https?:/i.test(source) || /url\(\s*["']?https?:/i.test(source))
161
+ problems.push("Remove remote stylesheets, fonts, and background images. The artifact must work offline.");
162
+
163
+ const { render, PeleError } = await import(pathToFileURL(resolveDependency("pele")).href);
164
+ const hljs = createRequire(import.meta.url)(resolveDependency("highlight.js"));
165
+ const font = /--font-sans\s*:\s*([^;]+);/.exec(source)?.[1]?.trim() ?? "sans-serif";
166
+
167
+ let diagrams = 0;
168
+ let html = source.replace(/<pre\b([^>]*\bclass\s*=\s*["'][^"']*\bmermaid\b[^"']*["'][^>]*)>([\s\S]*?)<\/pre>/gi, (_whole, _attrs, body) => {
169
+ diagrams++;
170
+ try {
171
+ return `<figure class="diagram">${render(decode(body).trim(), { fontFamily: font }).svg}</figure>`;
172
+ } catch (error) {
173
+ if (!(error instanceof PeleError)) throw error;
174
+ problems.push(`Diagram ${diagrams}: ${error.message}`);
175
+ return "";
176
+ }
177
+ });
178
+
179
+ let codeBlocks = 0;
180
+ html = html.replace(/<pre>\s*<code\b[^>]*\bclass\s*=\s*["'][^"']*\blanguage-([\w+-]+)[^"']*["'][^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (whole, language, body) => {
181
+ if (!hljs.getLanguage(language)) {
182
+ warnings.push(`Code block language "${language}" is unknown; left unhighlighted.`);
183
+ return whole;
184
+ }
185
+ codeBlocks++;
186
+ return `<pre class="code"><code class="hljs language-${language}">${hljs.highlight(decode(body), { language, ignoreIllegals: true }).value}</code></pre>`;
187
+ });
188
+
189
+ let images = 0;
190
+ html = html.replace(/(<img\b[^>]*\bsrc\s*=\s*)(["'])([^"']+)\2/gi, (whole, prefix, quote, src) => {
191
+ if (src.startsWith("data:")) return whole;
192
+ if (/^[a-z][a-z0-9+.-]*:/i.test(src)) {
193
+ problems.push(`Image ${src} is remote. Download it next to the source and use a relative path.`);
194
+ return whole;
195
+ }
196
+ const file = resolve(directory, decode(src));
197
+ const mime = MIME[extname(file).toLowerCase()];
198
+ if (!mime || !existsSync(file) || !statSync(file).isFile()) {
199
+ problems.push(`Image ${src} is missing or not a PNG, JPEG, GIF, SVG, or WebP file.`);
200
+ return whole;
201
+ }
202
+ images++;
203
+ return `${prefix}${quote}data:${mime};base64,${readFileSync(file).toString("base64")}${quote}`;
204
+ });
205
+
206
+ const slides = (html.match(/<section\b[^>]*\bclass\s*=\s*["'][^"']*\bslide\b/gi) ?? []).length;
207
+ if (problems.length) return { ok: false, problems, warnings };
208
+
209
+ const outDir = resolve(options.out ?? join(directory, "dist"));
210
+ mkdirSync(outDir, { recursive: true });
211
+ const name = basename(sourcePath).replace(/\.html?$/i, "");
212
+ const htmlPath = join(outDir, `${name}.html`);
213
+ writeFileSync(htmlPath, html);
214
+ const result = { ok: true, html: htmlPath, diagrams, codeBlocks, images, slides, warnings };
215
+ if (!options.pdf && !options.preview) return result;
216
+ const chrome = chromePath();
217
+ if (!chrome) return { ...result, ok: false, problems: ["No Chrome, Chromium, Edge, or Brave found. Install one or set CHROME_PATH."] };
218
+ result.layout = await checkLayout(chrome, htmlPath);
219
+ if (options.preview) {
220
+ // Screen layout: slides stack 900px frames with 40px gaps inside 40px padding; a document gets its first pages.
221
+ const size = slides ? `1680,${slides * 940 + 80}` : "900,3600";
222
+ const previewPath = join(outDir, `${name}.png`);
223
+ rmSync(previewPath, { force: true });
224
+ await runChrome(chrome, htmlPath, previewPath, ["--hide-scrollbars", `--window-size=${size}`, `--screenshot=${previewPath}`]);
225
+ result.preview = previewPath;
226
+ }
227
+ if (!options.pdf) return result;
228
+ const pdfPath = join(outDir, `${name}.pdf`);
229
+ rmSync(pdfPath, { force: true });
230
+ await runChrome(chrome, htmlPath, pdfPath, ["--no-pdf-header-footer", "--print-to-pdf-no-header", `--print-to-pdf=${pdfPath}`]);
231
+ const pages = pdfPages(pdfPath);
232
+ if (slides && pages !== undefined && pages !== slides)
233
+ warnings.push(`The PDF has ${pages} pages for ${slides} slides. A slide overflows its page; shorten it or split it.`);
234
+ return { ...result, pdf: pdfPath, pages };
235
+ }
236
+
237
+ const [command, sourceArg, ...rest] = process.argv.slice(2);
238
+ if (command !== "build" || !sourceArg) {
239
+ console.error("Usage: node artifact.mjs build <source.html> [--out <dir>] [--no-pdf] [--preview]");
240
+ process.exit(2);
241
+ }
242
+ const outIndex = rest.indexOf("--out");
243
+ try {
244
+ const result = await build(resolve(sourceArg), {
245
+ out: outIndex >= 0 ? rest[outIndex + 1] : undefined,
246
+ pdf: !rest.includes("--no-pdf"),
247
+ preview: rest.includes("--preview"),
248
+ });
249
+ console.log(JSON.stringify(result, null, 2));
250
+ process.exit(result.ok ? 0 : 1);
251
+ } catch (error) {
252
+ console.log(JSON.stringify({ ok: false, problems: [error instanceof Error ? error.message : String(error)], warnings: [] }, null, 2));
253
+ process.exit(1);
254
+ }
@@ -0,0 +1,55 @@
1
+ """Artifact helpers for the raga Python REPL. Load with exec(await read(path), globals()), then call artifact_setup."""
2
+
3
+ import json
4
+ import os
5
+ import shlex
6
+ import shutil
7
+
8
+ ARTIFACT_SKILL_DIR = None
9
+
10
+
11
+ def artifact_setup(skill_dir):
12
+ """Remember where this skill lives so the other helpers find the builder and templates."""
13
+ global ARTIFACT_SKILL_DIR
14
+ ARTIFACT_SKILL_DIR = os.path.abspath(skill_dir)
15
+
16
+
17
+ def _skill(path):
18
+ if ARTIFACT_SKILL_DIR is None:
19
+ raise RuntimeError("Call artifact_setup(<directory of the raga-artifacts SKILL.md>) first")
20
+ return os.path.join(ARTIFACT_SKILL_DIR, path)
21
+
22
+
23
+ def artifact_new(kind, path):
24
+ """Copy the slides or doc template to path. Refuses to overwrite an existing file."""
25
+ if kind not in ("slides", "doc"):
26
+ raise ValueError('kind must be "slides" or "doc"')
27
+ path = os.path.abspath(path)
28
+ if os.path.exists(path):
29
+ raise FileExistsError(path + " exists. Edit it, or choose another path")
30
+ os.makedirs(os.path.dirname(path), exist_ok=True)
31
+ shutil.copyfile(_skill(os.path.join("templates", kind + ".html")), path)
32
+ return path
33
+
34
+
35
+ async def artifact_build(source, pdf=True, preview=False, out=None):
36
+ """Build source into dist/<name>.html and, by default, dist/<name>.pdf. Returns the builder's JSON.
37
+
38
+ ok is False with a problems list when a diagram fails, the source has scripts or remote assets,
39
+ or no browser is found. preview=True also writes dist/<name>.png for a visual check.
40
+ """
41
+ command = "node " + shlex.quote(_skill("artifact.mjs")) + " build " + shlex.quote(os.path.abspath(source))
42
+ if out:
43
+ command += " --out " + shlex.quote(os.path.abspath(out))
44
+ if not pdf:
45
+ command += " --no-pdf"
46
+ if preview:
47
+ command += " --preview"
48
+ runner = globals().get("bash")
49
+ if runner is None:
50
+ raise RuntimeError("artifact_build runs in the raga Python REPL, where bash is available")
51
+ output = await runner(command + " 2>&1; true", timeout=300)
52
+ start = output.find("{")
53
+ if start < 0:
54
+ return {"ok": False, "problems": [output.strip()], "warnings": []}
55
+ return json.loads(output[start:])