@dev-sergeev/datalab-kit 0.1.5 → 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 (51) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +23 -1
  3. package/THIRD_PARTY_NOTICES.md +3 -0
  4. package/dist/extension.js +2 -0
  5. package/dist/skills.d.ts +2 -0
  6. package/dist/skills.js +54 -0
  7. package/package.json +36 -9
  8. package/skills/init/SKILL.md +31 -0
  9. package/skills/skill-creator/LICENSE.txt +202 -0
  10. package/skills/skill-creator/SKILL.md +610 -0
  11. package/skills/skill-creator/agents/analyzer.md +291 -0
  12. package/skills/skill-creator/agents/comparator.md +213 -0
  13. package/skills/skill-creator/agents/grader.md +234 -0
  14. package/skills/skill-creator/assets/eval_review.html +146 -0
  15. package/skills/skill-creator/eval-viewer/generate_review.py +453 -0
  16. package/skills/skill-creator/eval-viewer/viewer.html +1329 -0
  17. package/skills/skill-creator/references/schemas.md +521 -0
  18. package/skills/skill-creator/scripts/aggregate_benchmark.py +415 -0
  19. package/skills/skill-creator/scripts/generate_report.py +329 -0
  20. package/skills/skill-creator/scripts/improve_description.py +225 -0
  21. package/skills/skill-creator/scripts/package_skill.py +136 -0
  22. package/skills/skill-creator/scripts/pi_runtime.py +177 -0
  23. package/skills/skill-creator/scripts/quick_validate.py +107 -0
  24. package/skills/skill-creator/scripts/run_eval.py +215 -0
  25. package/skills/skill-creator/scripts/run_loop.py +334 -0
  26. package/skills/skill-creator/scripts/utils.py +41 -0
  27. package/node_modules/node-addon-api/common 2.gypi +0 -21
  28. package/node_modules/node-addon-api/except 2.gypi +0 -25
  29. package/node_modules/node-addon-api/napi 2.h +0 -3463
  30. package/node_modules/node-addon-api/napi-inl 2.h +0 -7250
  31. package/node_modules/node-addon-api/napi-inl.deprecated 2.h +0 -186
  32. package/node_modules/node-addon-api/node_addon_api 2.gyp +0 -42
  33. package/node_modules/node-addon-api/node_api 2.gyp +0 -9
  34. package/node_modules/node-addon-api/noexcept 2.gypi +0 -26
  35. package/node_modules/pi-toggle-skills/toggle-skills 2.ts +0 -316
  36. package/node_modules/pi-toggle-skills/vitest.config 2.ts +0 -15
  37. package/node_modules/sprintf-js/.npmignore 2 +0 -1
  38. package/node_modules/sprintf-js/LICENSE 2 +0 -24
  39. package/node_modules/sprintf-js/README 2.md +0 -88
  40. package/node_modules/sprintf-js/bower 2.json +0 -14
  41. package/node_modules/sprintf-js/gruntfile 2.js +0 -36
  42. package/node_modules/sprintf-js/package 2.json +0 -22
  43. package/node_modules/tree-sitter-bash/tree-sitter-bash 2.wasm +0 -0
  44. package/node_modules/typebox/license 2 +0 -23
  45. package/node_modules/typebox/package 2.json +0 -87
  46. package/node_modules/typebox/readme 2.md +0 -370
  47. package/node_modules/zod/compile 2.cjs +0 -96
  48. package/node_modules/zod/compile 2.js +0 -52
  49. package/node_modules/zod/index 2.cjs +0 -52
  50. package/node_modules/zod/index 2.js +0 -7
  51. /package/{node_modules/node-addon-api/nothing 2.c → skills/skill-creator/scripts/__init__.py} +0 -0
@@ -0,0 +1,177 @@
1
+ """Pi JSON CLI backend for the Anthropic skill-creator scripts.
2
+
3
+ Added by Datalab Kit for Pi; upstream source pinned at
4
+ anthropics/skills dbd4588f9e1033efb41dad4bef2f7947c8993d44 (Apache-2.0).
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import selectors
10
+ import signal
11
+ import subprocess
12
+ import tempfile
13
+ import threading
14
+ import time
15
+ from contextlib import contextmanager
16
+ from pathlib import Path
17
+
18
+
19
+ def pi_command(model: str | None = None) -> list[str]:
20
+ """Use the host Pi installation without importing package-local Node modules."""
21
+ # Keep configured extensions: they may register the selected model provider.
22
+ # Discovery isolation concerns skills/context, not the user's credentials.
23
+ command = [
24
+ os.environ.get("PI_EXECUTABLE_PATH") or "pi",
25
+ "--print", "--mode", "json", "--no-session",
26
+ "--no-skills", "--no-prompt-templates", "--no-themes",
27
+ "--no-context-files", "--no-approve",
28
+ # Empty explicit sources suppress global SYSTEM.md/APPEND_SYSTEM.md
29
+ # discovery while retaining Pi's built-in system prompt.
30
+ "--system-prompt", "", "--append-system-prompt", "",
31
+ ]
32
+ if model:
33
+ command.extend(["--model", model])
34
+ return command
35
+
36
+
37
+ @contextmanager
38
+ def interrupt_on_termination():
39
+ """Give CLI SIGTERM the same resource cleanup guarantees as Ctrl-C."""
40
+ if threading.current_thread() is not threading.main_thread():
41
+ yield
42
+ return
43
+ previous = signal.getsignal(signal.SIGTERM)
44
+
45
+ def terminate(signum, frame):
46
+ raise SystemExit(128 + signum)
47
+
48
+ signal.signal(signal.SIGTERM, terminate)
49
+ try:
50
+ yield
51
+ finally:
52
+ signal.signal(signal.SIGTERM, previous)
53
+
54
+
55
+ def _stop_process(process: subprocess.Popen) -> None:
56
+ """Reap Pi and terminate its subprocess group, including tool children."""
57
+ if os.name == "posix":
58
+ # Also kill descendants if the group leader has already exited.
59
+ try:
60
+ os.killpg(process.pid, signal.SIGTERM)
61
+ except ProcessLookupError:
62
+ pass
63
+ try:
64
+ process.wait(timeout=2)
65
+ except subprocess.TimeoutExpired:
66
+ pass
67
+ try:
68
+ os.killpg(process.pid, signal.SIGKILL)
69
+ except ProcessLookupError:
70
+ pass
71
+ elif process.poll() is None:
72
+ process.kill()
73
+ process.wait()
74
+
75
+
76
+ def run_pi(
77
+ prompt: str,
78
+ model: str | None,
79
+ timeout: int,
80
+ cwd: Path,
81
+ options: list[str],
82
+ on_event=None,
83
+ cancel: threading.Event | None = None,
84
+ ) -> str:
85
+ """Run one fresh Pi session, optionally stopping at a decisive tool event.
86
+
87
+ An on_event callback returning True marks an intentional early completion.
88
+ Otherwise a valid agent_end, successful process exit and non-error assistant
89
+ messages are required. Provider failures can exit zero in Pi JSON mode.
90
+ """
91
+ command = pi_command(model) + options
92
+ # A file-backed stdin avoids argument-size limits and pipe write deadlocks.
93
+ with tempfile.TemporaryFile() as stdin, tempfile.TemporaryFile() as stderr:
94
+ stdin.write(prompt.encode("utf-8"))
95
+ stdin.seek(0)
96
+ process = subprocess.Popen(
97
+ command, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr,
98
+ cwd=cwd, start_new_session=(os.name == "posix"),
99
+ )
100
+ deadline = time.monotonic() + timeout
101
+ text = []
102
+ ended = False
103
+ assistant_seen = False
104
+ buffer = b""
105
+
106
+ def consume(line):
107
+ nonlocal ended, assistant_seen
108
+ if not line.strip():
109
+ return False
110
+ try:
111
+ event = json.loads(line)
112
+ except (ValueError, UnicodeDecodeError) as error:
113
+ raise RuntimeError("Pi emitted invalid JSON on stdout") from error
114
+ if not isinstance(event, dict):
115
+ raise RuntimeError("Pi emitted a non-object JSON event")
116
+ if event.get("type") == "message_end":
117
+ message = event.get("message", {})
118
+ if message.get("role") == "assistant":
119
+ assistant_seen = True
120
+ if message.get("stopReason") in ("error", "aborted"):
121
+ raise RuntimeError(f"Pi model failed: {message.get('errorMessage') or message['stopReason']}")
122
+ text.extend(block["text"] for block in message.get("content", []) if block.get("type") == "text")
123
+ if event.get("type") == "agent_end":
124
+ ended = True
125
+ return bool(on_event and on_event(event))
126
+
127
+ try:
128
+ with selectors.DefaultSelector() as selector:
129
+ selector.register(process.stdout, selectors.EVENT_READ)
130
+ while True:
131
+ if cancel is not None and cancel.is_set():
132
+ raise RuntimeError("Pi evaluation cancelled")
133
+ remaining = deadline - time.monotonic()
134
+ if remaining <= 0:
135
+ raise TimeoutError(f"Pi did not complete within {timeout} seconds")
136
+ if not selector.select(min(remaining, 0.1)):
137
+ continue
138
+ chunk = os.read(process.stdout.fileno(), 65536)
139
+ if not chunk:
140
+ if buffer and consume(buffer):
141
+ return "".join(text)
142
+ break
143
+ buffer += chunk
144
+ while b"\n" in buffer:
145
+ line, buffer = buffer.split(b"\n", 1)
146
+ if consume(line):
147
+ return "".join(text)
148
+ remaining = deadline - time.monotonic()
149
+ if remaining <= 0:
150
+ raise TimeoutError(f"Pi did not complete within {timeout} seconds")
151
+ try:
152
+ returncode = process.wait(timeout=remaining)
153
+ except subprocess.TimeoutExpired as error:
154
+ raise TimeoutError(f"Pi did not complete within {timeout} seconds") from error
155
+ if returncode != 0:
156
+ raise RuntimeError(f"Pi exited {returncode}")
157
+ if not ended or not assistant_seen:
158
+ raise RuntimeError("Pi exited without a complete assistant session")
159
+ return "".join(text)
160
+ except Exception as error:
161
+ stderr.seek(0)
162
+ diagnostic = stderr.read().decode("utf-8", errors="replace").strip()
163
+ if diagnostic:
164
+ raise RuntimeError(f"{error}\nPi stderr: {diagnostic}") from error
165
+ raise
166
+ finally:
167
+ _stop_process(process)
168
+ process.stdout.close()
169
+
170
+
171
+ def call_pi(prompt: str, model: str | None, timeout: int = 300) -> str:
172
+ """Generate text without tools, skill discovery or writes in the package."""
173
+ with tempfile.TemporaryDirectory(prefix="pi-skill-description-") as workdir:
174
+ response = run_pi(prompt, model, timeout, Path(workdir), ["--no-tools"])
175
+ if not response.strip():
176
+ raise RuntimeError("Pi returned no description text")
177
+ return response
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Quick validation script for skills - minimal version
4
+
5
+ Changed by Datalab Kit: accept Pi's disable-model-invocation metadata.
6
+ Upstream: anthropics/skills dbd4588f9e1033efb41dad4bef2f7947c8993d44
7
+ (Apache-2.0).
8
+ """
9
+
10
+ import sys
11
+ import os
12
+ import re
13
+ import yaml
14
+ from pathlib import Path
15
+
16
+ def validate_skill(skill_path):
17
+ """Basic validation of a skill"""
18
+ skill_path = Path(skill_path)
19
+
20
+ # Check SKILL.md exists
21
+ skill_md = skill_path / 'SKILL.md'
22
+ if not skill_md.exists():
23
+ return False, "SKILL.md not found"
24
+
25
+ # Read and validate frontmatter
26
+ content = skill_md.read_text()
27
+ if not content.startswith('---'):
28
+ return False, "No YAML frontmatter found"
29
+
30
+ # Extract frontmatter
31
+ match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
32
+ if not match:
33
+ return False, "Invalid frontmatter format"
34
+
35
+ frontmatter_text = match.group(1)
36
+
37
+ # Parse YAML frontmatter
38
+ try:
39
+ frontmatter = yaml.safe_load(frontmatter_text)
40
+ if not isinstance(frontmatter, dict):
41
+ return False, "Frontmatter must be a YAML dictionary"
42
+ except yaml.YAMLError as e:
43
+ return False, f"Invalid YAML in frontmatter: {e}"
44
+
45
+ # Define allowed properties
46
+ ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility', 'disable-model-invocation'}
47
+
48
+ # Check for unexpected properties (excluding nested keys under metadata)
49
+ unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
50
+ if unexpected_keys:
51
+ return False, (
52
+ f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
53
+ f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
54
+ )
55
+
56
+ # Check required fields
57
+ if 'name' not in frontmatter:
58
+ return False, "Missing 'name' in frontmatter"
59
+ if 'description' not in frontmatter:
60
+ return False, "Missing 'description' in frontmatter"
61
+
62
+ # Extract name for validation
63
+ name = frontmatter.get('name', '')
64
+ if not isinstance(name, str):
65
+ return False, f"Name must be a string, got {type(name).__name__}"
66
+ name = name.strip()
67
+ if name:
68
+ # Check naming convention (kebab-case: lowercase with hyphens)
69
+ if not re.match(r'^[a-z0-9-]+$', name):
70
+ return False, f"Name '{name}' should be kebab-case (lowercase letters, digits, and hyphens only)"
71
+ if name.startswith('-') or name.endswith('-') or '--' in name:
72
+ return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
73
+ # Check name length (max 64 characters per spec)
74
+ if len(name) > 64:
75
+ return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
76
+
77
+ # Extract and validate description
78
+ description = frontmatter.get('description', '')
79
+ if not isinstance(description, str):
80
+ return False, f"Description must be a string, got {type(description).__name__}"
81
+ description = description.strip()
82
+ if description:
83
+ # Check for angle brackets
84
+ if '<' in description or '>' in description:
85
+ return False, "Description cannot contain angle brackets (< or >)"
86
+ # Check description length (max 1024 characters per spec)
87
+ if len(description) > 1024:
88
+ return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
89
+
90
+ # Validate compatibility field if present (optional)
91
+ compatibility = frontmatter.get('compatibility', '')
92
+ if compatibility:
93
+ if not isinstance(compatibility, str):
94
+ return False, f"Compatibility must be a string, got {type(compatibility).__name__}"
95
+ if len(compatibility) > 500:
96
+ return False, f"Compatibility is too long ({len(compatibility)} characters). Maximum is 500 characters."
97
+
98
+ return True, "Skill is valid!"
99
+
100
+ if __name__ == "__main__":
101
+ if len(sys.argv) != 2:
102
+ print("Usage: python quick_validate.py <skill_directory>")
103
+ sys.exit(1)
104
+
105
+ valid, message = validate_skill(sys.argv[1])
106
+ print(message)
107
+ sys.exit(0 if valid else 1)
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+ """Run trigger evaluation for a skill description.
3
+
4
+ Tests whether a skill's description causes Pi to trigger (read the skill)
5
+ for a set of queries. Outputs results as JSON.
6
+
7
+ Changed by Datalab Kit: isolated Pi JSON CLI backend, error propagation and
8
+ cancellation cleanup. Upstream: anthropics/skills
9
+ dbd4588f9e1033efb41dad4bef2f7947c8993d44 (Apache-2.0).
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ import threading
16
+ import tempfile
17
+ from concurrent.futures import ThreadPoolExecutor, as_completed
18
+ from pathlib import Path
19
+
20
+ import yaml
21
+
22
+ from scripts.pi_runtime import interrupt_on_termination, run_pi
23
+
24
+ from scripts.utils import parse_skill_md
25
+
26
+
27
+ def find_project_root() -> Path:
28
+ """Use the caller's workspace; no host-specific discovery files are written."""
29
+ return Path.cwd()
30
+
31
+
32
+ def run_single_query(
33
+ query: str,
34
+ skill_name: str,
35
+ skill_description: str,
36
+ timeout: int,
37
+ project_root: str,
38
+ model: str | None = None,
39
+ cancel: threading.Event | None = None,
40
+ ) -> bool:
41
+ """Evaluate natural skill selection in a fresh, isolated Pi session.
42
+
43
+ Only the candidate description is advertised, not the creator or any other
44
+ installed skills. The read-only tool allowlist prevents evaluation queries
45
+ from modifying the caller's workspace. A read of this exact SKILL.md is
46
+ the trigger signal; narration and reads of unrelated files do not count.
47
+ """
48
+ with tempfile.TemporaryDirectory(prefix="pi-skill-trigger-") as workdir:
49
+ root = Path(workdir)
50
+ candidate = root / "SKILL.md"
51
+ frontmatter = yaml.safe_dump(
52
+ {"name": skill_name, "description": skill_description},
53
+ sort_keys=False, allow_unicode=True,
54
+ )
55
+ candidate.write_text(
56
+ f"---\n{frontmatter}---\n\n# {skill_name}\n\n"
57
+ "This is a description-trigger evaluation. The skill has been loaded.\n",
58
+ encoding="utf-8",
59
+ )
60
+ triggered = False
61
+
62
+ def detect_trigger(event):
63
+ nonlocal triggered
64
+ if event.get("type") != "tool_execution_start" or event.get("toolName") != "read":
65
+ return False
66
+ path = event.get("args", {}).get("path")
67
+ if not isinstance(path, str):
68
+ return False
69
+ requested = Path(path).expanduser()
70
+ if not requested.is_absolute():
71
+ requested = root / requested
72
+ triggered = requested.resolve() == candidate.resolve()
73
+ return triggered
74
+
75
+ run_pi(
76
+ query, model, timeout, root,
77
+ ["--skill", str(candidate), "--tools", "read"],
78
+ on_event=detect_trigger, cancel=cancel,
79
+ )
80
+ return triggered
81
+
82
+
83
+ def run_eval(
84
+ eval_set: list[dict],
85
+ skill_name: str,
86
+ description: str,
87
+ num_workers: int,
88
+ timeout: int,
89
+ project_root: Path,
90
+ runs_per_query: int = 1,
91
+ trigger_threshold: float = 0.5,
92
+ model: str | None = None,
93
+ ) -> dict:
94
+ """Run the full eval set and return results."""
95
+ results = []
96
+
97
+ cancel = threading.Event()
98
+ executor = ThreadPoolExecutor(max_workers=num_workers)
99
+ try:
100
+ future_to_info = {}
101
+ for item in eval_set:
102
+ for run_idx in range(runs_per_query):
103
+ future = executor.submit(
104
+ run_single_query,
105
+ item["query"],
106
+ skill_name,
107
+ description,
108
+ timeout,
109
+ str(project_root),
110
+ model,
111
+ cancel,
112
+ )
113
+ future_to_info[future] = (item, run_idx)
114
+
115
+ query_triggers: dict[str, list[bool]] = {}
116
+ query_items: dict[str, dict] = {}
117
+ for future in as_completed(future_to_info):
118
+ item, _ = future_to_info[future]
119
+ query = item["query"]
120
+ query_items[query] = item
121
+ if query not in query_triggers:
122
+ query_triggers[query] = []
123
+ try:
124
+ query_triggers[query].append(future.result())
125
+ except Exception as error:
126
+ raise RuntimeError(f"Trigger evaluation failed for {query!r}: {error}") from error
127
+ finally:
128
+ cancel.set()
129
+ executor.shutdown(wait=True, cancel_futures=True)
130
+
131
+ for query, triggers in query_triggers.items():
132
+ item = query_items[query]
133
+ trigger_rate = sum(triggers) / len(triggers)
134
+ should_trigger = item["should_trigger"]
135
+ if should_trigger:
136
+ did_pass = trigger_rate >= trigger_threshold
137
+ else:
138
+ did_pass = trigger_rate < trigger_threshold
139
+ results.append({
140
+ "query": query,
141
+ "should_trigger": should_trigger,
142
+ "trigger_rate": trigger_rate,
143
+ "triggers": sum(triggers),
144
+ "runs": len(triggers),
145
+ "pass": did_pass,
146
+ })
147
+
148
+ passed = sum(1 for r in results if r["pass"])
149
+ total = len(results)
150
+
151
+ return {
152
+ "skill_name": skill_name,
153
+ "description": description,
154
+ "results": results,
155
+ "summary": {
156
+ "total": total,
157
+ "passed": passed,
158
+ "failed": total - passed,
159
+ },
160
+ }
161
+
162
+
163
+ def main():
164
+ parser = argparse.ArgumentParser(description="Run trigger evaluation for a skill description")
165
+ parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file")
166
+ parser.add_argument("--skill-path", required=True, help="Path to skill directory")
167
+ parser.add_argument("--description", default=None, help="Override description to test")
168
+ parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers")
169
+ parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds")
170
+ parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query")
171
+ parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold")
172
+ parser.add_argument("--model", default=None, help="Pi model provider/id[:thinking] (default: configured model)")
173
+ parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
174
+ args = parser.parse_args()
175
+
176
+ eval_set = json.loads(Path(args.eval_set).read_text())
177
+ skill_path = Path(args.skill_path)
178
+
179
+ if not (skill_path / "SKILL.md").exists():
180
+ print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
181
+ sys.exit(1)
182
+
183
+ name, original_description, content = parse_skill_md(skill_path)
184
+ description = args.description or original_description
185
+ project_root = find_project_root()
186
+
187
+ if args.verbose:
188
+ print(f"Evaluating: {description}", file=sys.stderr)
189
+
190
+ output = run_eval(
191
+ eval_set=eval_set,
192
+ skill_name=name,
193
+ description=description,
194
+ num_workers=args.num_workers,
195
+ timeout=args.timeout,
196
+ project_root=project_root,
197
+ runs_per_query=args.runs_per_query,
198
+ trigger_threshold=args.trigger_threshold,
199
+ model=args.model,
200
+ )
201
+
202
+ if args.verbose:
203
+ summary = output["summary"]
204
+ print(f"Results: {summary['passed']}/{summary['total']} passed", file=sys.stderr)
205
+ for r in output["results"]:
206
+ status = "PASS" if r["pass"] else "FAIL"
207
+ rate_str = f"{r['triggers']}/{r['runs']}"
208
+ print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:70]}", file=sys.stderr)
209
+
210
+ print(json.dumps(output, indent=2))
211
+
212
+
213
+ if __name__ == "__main__":
214
+ with interrupt_on_termination():
215
+ main()