@dev-sergeev/datalab-kit 0.1.4 → 0.1.6

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 (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +47 -4
  3. package/THIRD_PARTY_NOTICES.md +3 -0
  4. package/dist/datalab-provider.d.ts +2 -0
  5. package/dist/datalab-provider.js +45 -0
  6. package/dist/extension.js +4 -0
  7. package/dist/skills.d.ts +2 -0
  8. package/dist/skills.js +54 -0
  9. package/docs/adr/0003-datalab-environment-provider.md +11 -0
  10. package/package.json +36 -9
  11. package/skills/init/SKILL.md +31 -0
  12. package/skills/skill-creator/LICENSE.txt +202 -0
  13. package/skills/skill-creator/SKILL.md +610 -0
  14. package/skills/skill-creator/agents/analyzer.md +291 -0
  15. package/skills/skill-creator/agents/comparator.md +213 -0
  16. package/skills/skill-creator/agents/grader.md +234 -0
  17. package/skills/skill-creator/assets/eval_review.html +146 -0
  18. package/skills/skill-creator/eval-viewer/generate_review.py +453 -0
  19. package/skills/skill-creator/eval-viewer/viewer.html +1329 -0
  20. package/skills/skill-creator/references/schemas.md +521 -0
  21. package/skills/skill-creator/scripts/aggregate_benchmark.py +415 -0
  22. package/skills/skill-creator/scripts/generate_report.py +329 -0
  23. package/skills/skill-creator/scripts/improve_description.py +225 -0
  24. package/skills/skill-creator/scripts/package_skill.py +136 -0
  25. package/skills/skill-creator/scripts/pi_runtime.py +177 -0
  26. package/skills/skill-creator/scripts/quick_validate.py +107 -0
  27. package/skills/skill-creator/scripts/run_eval.py +215 -0
  28. package/skills/skill-creator/scripts/run_loop.py +334 -0
  29. package/skills/skill-creator/scripts/utils.py +41 -0
  30. package/node_modules/node-addon-api/common 2.gypi +0 -21
  31. package/node_modules/node-addon-api/except 2.gypi +0 -25
  32. package/node_modules/node-addon-api/napi 2.h +0 -3463
  33. package/node_modules/node-addon-api/napi-inl 2.h +0 -7250
  34. package/node_modules/node-addon-api/napi-inl.deprecated 2.h +0 -186
  35. package/node_modules/node-addon-api/node_addon_api 2.gyp +0 -42
  36. package/node_modules/node-addon-api/node_api 2.gyp +0 -9
  37. package/node_modules/node-addon-api/noexcept 2.gypi +0 -26
  38. package/node_modules/pi-toggle-skills/toggle-skills 2.ts +0 -316
  39. package/node_modules/pi-toggle-skills/vitest.config 2.ts +0 -15
  40. package/node_modules/sprintf-js/.npmignore 2 +0 -1
  41. package/node_modules/sprintf-js/LICENSE 2 +0 -24
  42. package/node_modules/sprintf-js/README 2.md +0 -88
  43. package/node_modules/sprintf-js/bower 2.json +0 -14
  44. package/node_modules/sprintf-js/gruntfile 2.js +0 -36
  45. package/node_modules/sprintf-js/package 2.json +0 -22
  46. package/node_modules/tree-sitter-bash/tree-sitter-bash 2.wasm +0 -0
  47. package/node_modules/typebox/license 2 +0 -23
  48. package/node_modules/typebox/package 2.json +0 -87
  49. package/node_modules/typebox/readme 2.md +0 -370
  50. package/node_modules/zod/compile 2.cjs +0 -96
  51. package/node_modules/zod/compile 2.js +0 -52
  52. package/node_modules/zod/index 2.cjs +0 -52
  53. package/node_modules/zod/index 2.js +0 -7
  54. /package/{node_modules/node-addon-api/nothing 2.c → skills/skill-creator/scripts/__init__.py} +0 -0
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env python3
2
+ """Generate and serve a review page for eval results.
3
+
4
+ Reads the workspace directory, discovers runs (directories with outputs/),
5
+ embeds all output data into a self-contained HTML page, and serves it via
6
+ a tiny HTTP server. Feedback auto-saves to feedback.json in the workspace.
7
+
8
+ Usage:
9
+ python generate_review.py <workspace-path> [--port PORT] [--skill-name NAME]
10
+ python generate_review.py <workspace-path> --previous-feedback /path/to/old/feedback.json
11
+
12
+ No dependencies beyond the Python stdlib are required.
13
+
14
+ Changed by Datalab Kit: resolve nested run metadata and leave occupied ports alone.
15
+ Upstream: anthropics/skills dbd4588f9e1033efb41dad4bef2f7947c8993d44
16
+ (Apache-2.0).
17
+ """
18
+
19
+ import argparse
20
+ import base64
21
+ import json
22
+ import mimetypes
23
+ import re
24
+ import sys
25
+ import webbrowser
26
+ from functools import partial
27
+ from http.server import HTTPServer, BaseHTTPRequestHandler
28
+ from pathlib import Path
29
+
30
+ # Files to exclude from output listings
31
+ METADATA_FILES = {"transcript.md", "user_notes.md", "metrics.json"}
32
+
33
+ # Extensions we render as inline text
34
+ TEXT_EXTENSIONS = {
35
+ ".txt", ".md", ".json", ".csv", ".py", ".js", ".ts", ".tsx", ".jsx",
36
+ ".yaml", ".yml", ".xml", ".html", ".css", ".sh", ".rb", ".go", ".rs",
37
+ ".java", ".c", ".cpp", ".h", ".hpp", ".sql", ".r", ".toml",
38
+ }
39
+
40
+ # Extensions we render as inline images
41
+ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"}
42
+
43
+ # MIME type overrides for common types
44
+ MIME_OVERRIDES = {
45
+ ".svg": "image/svg+xml",
46
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
47
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
48
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
49
+ }
50
+
51
+
52
+ def get_mime_type(path: Path) -> str:
53
+ ext = path.suffix.lower()
54
+ if ext in MIME_OVERRIDES:
55
+ return MIME_OVERRIDES[ext]
56
+ mime, _ = mimetypes.guess_type(str(path))
57
+ return mime or "application/octet-stream"
58
+
59
+
60
+ def find_runs(workspace: Path) -> list[dict]:
61
+ """Recursively find directories that contain an outputs/ subdirectory."""
62
+ runs: list[dict] = []
63
+ _find_runs_recursive(workspace, workspace, runs)
64
+ runs.sort(key=lambda r: (r.get("eval_id", float("inf")), r["id"]))
65
+ return runs
66
+
67
+
68
+ def _find_runs_recursive(root: Path, current: Path, runs: list[dict]) -> None:
69
+ if not current.is_dir():
70
+ return
71
+
72
+ outputs_dir = current / "outputs"
73
+ if outputs_dir.is_dir():
74
+ run = build_run(root, current)
75
+ if run:
76
+ runs.append(run)
77
+ return
78
+
79
+ skip = {"node_modules", ".git", "__pycache__", "skill", "inputs"}
80
+ for child in sorted(current.iterdir()):
81
+ if child.is_dir() and child.name not in skip:
82
+ _find_runs_recursive(root, child, runs)
83
+
84
+
85
+ def build_run(root: Path, run_dir: Path) -> dict | None:
86
+ """Build a run dict with prompt, outputs, and grading data."""
87
+ prompt = ""
88
+ eval_id = None
89
+
90
+ # Try eval_metadata.json
91
+ for directory in [run_dir, *run_dir.parents]:
92
+ if not directory.is_relative_to(root):
93
+ break
94
+ candidate = directory / "eval_metadata.json"
95
+ if candidate.exists():
96
+ try:
97
+ metadata = json.loads(candidate.read_text())
98
+ prompt = metadata.get("prompt", "")
99
+ eval_id = metadata.get("eval_id")
100
+ except (json.JSONDecodeError, OSError):
101
+ pass
102
+ if prompt:
103
+ break
104
+
105
+ # Fall back to transcript.md
106
+ if not prompt:
107
+ for candidate in [run_dir / "transcript.md", run_dir / "outputs" / "transcript.md"]:
108
+ if candidate.exists():
109
+ try:
110
+ text = candidate.read_text()
111
+ match = re.search(r"## Eval Prompt\n\n([\s\S]*?)(?=\n##|$)", text)
112
+ if match:
113
+ prompt = match.group(1).strip()
114
+ except OSError:
115
+ pass
116
+ if prompt:
117
+ break
118
+
119
+ if not prompt:
120
+ prompt = "(No prompt found)"
121
+
122
+ run_id = str(run_dir.relative_to(root)).replace("/", "-").replace("\\", "-")
123
+
124
+ # Collect output files
125
+ outputs_dir = run_dir / "outputs"
126
+ output_files: list[dict] = []
127
+ if outputs_dir.is_dir():
128
+ for f in sorted(outputs_dir.iterdir()):
129
+ if f.is_file() and f.name not in METADATA_FILES:
130
+ output_files.append(embed_file(f))
131
+
132
+ # Load grading if present
133
+ grading = None
134
+ for candidate in [run_dir / "grading.json", run_dir.parent / "grading.json"]:
135
+ if candidate.exists():
136
+ try:
137
+ grading = json.loads(candidate.read_text())
138
+ except (json.JSONDecodeError, OSError):
139
+ pass
140
+ if grading:
141
+ break
142
+
143
+ return {
144
+ "id": run_id,
145
+ "prompt": prompt,
146
+ "eval_id": eval_id,
147
+ "outputs": output_files,
148
+ "grading": grading,
149
+ }
150
+
151
+
152
+ def embed_file(path: Path) -> dict:
153
+ """Read a file and return an embedded representation."""
154
+ ext = path.suffix.lower()
155
+ mime = get_mime_type(path)
156
+
157
+ if ext in TEXT_EXTENSIONS:
158
+ try:
159
+ content = path.read_text(errors="replace")
160
+ except OSError:
161
+ content = "(Error reading file)"
162
+ return {
163
+ "name": path.name,
164
+ "type": "text",
165
+ "content": content,
166
+ }
167
+ elif ext in IMAGE_EXTENSIONS:
168
+ try:
169
+ raw = path.read_bytes()
170
+ b64 = base64.b64encode(raw).decode("ascii")
171
+ except OSError:
172
+ return {"name": path.name, "type": "error", "content": "(Error reading file)"}
173
+ return {
174
+ "name": path.name,
175
+ "type": "image",
176
+ "mime": mime,
177
+ "data_uri": f"data:{mime};base64,{b64}",
178
+ }
179
+ elif ext == ".pdf":
180
+ try:
181
+ raw = path.read_bytes()
182
+ b64 = base64.b64encode(raw).decode("ascii")
183
+ except OSError:
184
+ return {"name": path.name, "type": "error", "content": "(Error reading file)"}
185
+ return {
186
+ "name": path.name,
187
+ "type": "pdf",
188
+ "data_uri": f"data:{mime};base64,{b64}",
189
+ }
190
+ elif ext == ".xlsx":
191
+ try:
192
+ raw = path.read_bytes()
193
+ b64 = base64.b64encode(raw).decode("ascii")
194
+ except OSError:
195
+ return {"name": path.name, "type": "error", "content": "(Error reading file)"}
196
+ return {
197
+ "name": path.name,
198
+ "type": "xlsx",
199
+ "data_b64": b64,
200
+ }
201
+ else:
202
+ # Binary / unknown — base64 download link
203
+ try:
204
+ raw = path.read_bytes()
205
+ b64 = base64.b64encode(raw).decode("ascii")
206
+ except OSError:
207
+ return {"name": path.name, "type": "error", "content": "(Error reading file)"}
208
+ return {
209
+ "name": path.name,
210
+ "type": "binary",
211
+ "mime": mime,
212
+ "data_uri": f"data:{mime};base64,{b64}",
213
+ }
214
+
215
+
216
+ def load_previous_iteration(workspace: Path) -> dict[str, dict]:
217
+ """Load previous iteration's feedback and outputs.
218
+
219
+ Returns a map of run_id -> {"feedback": str, "outputs": list[dict]}.
220
+ """
221
+ result: dict[str, dict] = {}
222
+
223
+ # Load feedback
224
+ feedback_map: dict[str, str] = {}
225
+ feedback_path = workspace / "feedback.json"
226
+ if feedback_path.exists():
227
+ try:
228
+ data = json.loads(feedback_path.read_text())
229
+ feedback_map = {
230
+ r["run_id"]: r["feedback"]
231
+ for r in data.get("reviews", [])
232
+ if r.get("feedback", "").strip()
233
+ }
234
+ except (json.JSONDecodeError, OSError, KeyError):
235
+ pass
236
+
237
+ # Load runs (to get outputs)
238
+ prev_runs = find_runs(workspace)
239
+ for run in prev_runs:
240
+ result[run["id"]] = {
241
+ "feedback": feedback_map.get(run["id"], ""),
242
+ "outputs": run.get("outputs", []),
243
+ }
244
+
245
+ # Also add feedback for run_ids that had feedback but no matching run
246
+ for run_id, fb in feedback_map.items():
247
+ if run_id not in result:
248
+ result[run_id] = {"feedback": fb, "outputs": []}
249
+
250
+ return result
251
+
252
+
253
+ def generate_html(
254
+ runs: list[dict],
255
+ skill_name: str,
256
+ previous: dict[str, dict] | None = None,
257
+ benchmark: dict | None = None,
258
+ ) -> str:
259
+ """Generate the complete standalone HTML page with embedded data."""
260
+ template_path = Path(__file__).parent / "viewer.html"
261
+ template = template_path.read_text()
262
+
263
+ # Build previous_feedback and previous_outputs maps for the template
264
+ previous_feedback: dict[str, str] = {}
265
+ previous_outputs: dict[str, list[dict]] = {}
266
+ if previous:
267
+ for run_id, data in previous.items():
268
+ if data.get("feedback"):
269
+ previous_feedback[run_id] = data["feedback"]
270
+ if data.get("outputs"):
271
+ previous_outputs[run_id] = data["outputs"]
272
+
273
+ embedded = {
274
+ "skill_name": skill_name,
275
+ "runs": runs,
276
+ "previous_feedback": previous_feedback,
277
+ "previous_outputs": previous_outputs,
278
+ }
279
+ if benchmark:
280
+ embedded["benchmark"] = benchmark
281
+
282
+ data_json = json.dumps(embedded)
283
+
284
+ return template.replace("/*__EMBEDDED_DATA__*/", f"const EMBEDDED_DATA = {data_json};")
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # HTTP server (stdlib only, zero dependencies)
289
+ # ---------------------------------------------------------------------------
290
+
291
+
292
+ class ReviewHandler(BaseHTTPRequestHandler):
293
+ """Serves the review HTML and handles feedback saves.
294
+
295
+ Regenerates the HTML on each page load so that refreshing the browser
296
+ picks up new eval outputs without restarting the server.
297
+ """
298
+
299
+ def __init__(
300
+ self,
301
+ workspace: Path,
302
+ skill_name: str,
303
+ feedback_path: Path,
304
+ previous: dict[str, dict],
305
+ benchmark_path: Path | None,
306
+ *args,
307
+ **kwargs,
308
+ ):
309
+ self.workspace = workspace
310
+ self.skill_name = skill_name
311
+ self.feedback_path = feedback_path
312
+ self.previous = previous
313
+ self.benchmark_path = benchmark_path
314
+ super().__init__(*args, **kwargs)
315
+
316
+ def do_GET(self) -> None:
317
+ if self.path == "/" or self.path == "/index.html":
318
+ # Regenerate HTML on each request (re-scans workspace for new outputs)
319
+ runs = find_runs(self.workspace)
320
+ benchmark = None
321
+ if self.benchmark_path and self.benchmark_path.exists():
322
+ try:
323
+ benchmark = json.loads(self.benchmark_path.read_text())
324
+ except (json.JSONDecodeError, OSError):
325
+ pass
326
+ html = generate_html(runs, self.skill_name, self.previous, benchmark)
327
+ content = html.encode("utf-8")
328
+ self.send_response(200)
329
+ self.send_header("Content-Type", "text/html; charset=utf-8")
330
+ self.send_header("Content-Length", str(len(content)))
331
+ self.end_headers()
332
+ self.wfile.write(content)
333
+ elif self.path == "/api/feedback":
334
+ data = b"{}"
335
+ if self.feedback_path.exists():
336
+ data = self.feedback_path.read_bytes()
337
+ self.send_response(200)
338
+ self.send_header("Content-Type", "application/json")
339
+ self.send_header("Content-Length", str(len(data)))
340
+ self.end_headers()
341
+ self.wfile.write(data)
342
+ else:
343
+ self.send_error(404)
344
+
345
+ def do_POST(self) -> None:
346
+ if self.path == "/api/feedback":
347
+ length = int(self.headers.get("Content-Length", 0))
348
+ body = self.rfile.read(length)
349
+ try:
350
+ data = json.loads(body)
351
+ if not isinstance(data, dict) or "reviews" not in data:
352
+ raise ValueError("Expected JSON object with 'reviews' key")
353
+ self.feedback_path.write_text(json.dumps(data, indent=2) + "\n")
354
+ resp = b'{"ok":true}'
355
+ self.send_response(200)
356
+ except (json.JSONDecodeError, OSError, ValueError) as e:
357
+ resp = json.dumps({"error": str(e)}).encode()
358
+ self.send_response(500)
359
+ self.send_header("Content-Type", "application/json")
360
+ self.send_header("Content-Length", str(len(resp)))
361
+ self.end_headers()
362
+ self.wfile.write(resp)
363
+ else:
364
+ self.send_error(404)
365
+
366
+ def log_message(self, format: str, *args: object) -> None:
367
+ # Suppress request logging to keep terminal clean
368
+ pass
369
+
370
+
371
+ def main() -> None:
372
+ parser = argparse.ArgumentParser(description="Generate and serve eval review")
373
+ parser.add_argument("workspace", type=Path, help="Path to workspace directory")
374
+ parser.add_argument("--port", "-p", type=int, default=3117, help="Server port (default: 3117)")
375
+ parser.add_argument("--skill-name", "-n", type=str, default=None, help="Skill name for header")
376
+ parser.add_argument(
377
+ "--previous-workspace", type=Path, default=None,
378
+ help="Path to previous iteration's workspace (shows old outputs and feedback as context)",
379
+ )
380
+ parser.add_argument(
381
+ "--benchmark", type=Path, default=None,
382
+ help="Path to benchmark.json to show in the Benchmark tab",
383
+ )
384
+ parser.add_argument(
385
+ "--static", "-s", type=Path, default=None,
386
+ help="Write standalone HTML to this path instead of starting a server",
387
+ )
388
+ args = parser.parse_args()
389
+
390
+ workspace = args.workspace.resolve()
391
+ if not workspace.is_dir():
392
+ print(f"Error: {workspace} is not a directory", file=sys.stderr)
393
+ sys.exit(1)
394
+
395
+ runs = find_runs(workspace)
396
+ if not runs:
397
+ print(f"No runs found in {workspace}", file=sys.stderr)
398
+ sys.exit(1)
399
+
400
+ skill_name = args.skill_name or workspace.name.replace("-workspace", "")
401
+ feedback_path = workspace / "feedback.json"
402
+
403
+ previous: dict[str, dict] = {}
404
+ if args.previous_workspace:
405
+ previous = load_previous_iteration(args.previous_workspace.resolve())
406
+
407
+ benchmark_path = args.benchmark.resolve() if args.benchmark else None
408
+ benchmark = None
409
+ if benchmark_path and benchmark_path.exists():
410
+ try:
411
+ benchmark = json.loads(benchmark_path.read_text())
412
+ except (json.JSONDecodeError, OSError):
413
+ pass
414
+
415
+ if args.static:
416
+ html = generate_html(runs, skill_name, previous, benchmark)
417
+ args.static.parent.mkdir(parents=True, exist_ok=True)
418
+ args.static.write_text(html)
419
+ print(f"\n Static viewer written to: {args.static}\n")
420
+ sys.exit(0)
421
+
422
+ port = args.port
423
+ handler = partial(ReviewHandler, workspace, skill_name, feedback_path, previous, benchmark_path)
424
+ try:
425
+ server = HTTPServer(("127.0.0.1", port), handler)
426
+ except OSError:
427
+ # Leave an existing listener alone and choose an available port.
428
+ server = HTTPServer(("127.0.0.1", 0), handler)
429
+ port = server.server_address[1]
430
+
431
+ url = f"http://localhost:{port}"
432
+ print(f"\n Eval Viewer")
433
+ print(f" ─────────────────────────────────")
434
+ print(f" URL: {url}")
435
+ print(f" Workspace: {workspace}")
436
+ print(f" Feedback: {feedback_path}")
437
+ if previous:
438
+ print(f" Previous: {args.previous_workspace} ({len(previous)} runs)")
439
+ if benchmark_path:
440
+ print(f" Benchmark: {benchmark_path}")
441
+ print(f"\n Press Ctrl+C to stop.\n")
442
+
443
+ webbrowser.open(url)
444
+
445
+ try:
446
+ server.serve_forever()
447
+ except KeyboardInterrupt:
448
+ print("\nStopped.")
449
+ server.server_close()
450
+
451
+
452
+ if __name__ == "__main__":
453
+ main()