cue-ai 0.9.0 → 0.9.2
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/CHANGELOG.md +40 -0
- package/README.md +82 -33
- package/bin/cue-review-progress +107 -0
- package/bin/cue-review-watch +98 -0
- package/dist/cue.js +7352 -3744
- package/package.json +16 -5
- package/profiles/_types.ts +9 -0
- package/profiles/backend/profile.yaml +2 -0
- package/profiles/blog-writer/profile.yaml +10 -0
- package/profiles/browser/profile.yaml +9 -2
- package/profiles/builder/profile.yaml +3 -6
- package/profiles/career/profile.yaml +13 -2
- package/profiles/claude-api/profile.yaml +1 -1
- package/profiles/commerce/profile.yaml +27 -3
- package/profiles/core/logo.png +0 -0
- package/profiles/core/profile.yaml +62 -2
- package/profiles/dash-merge-test/profile.yaml +109 -0
- package/profiles/designer/profile.yaml +2 -0
- package/profiles/designer-medusa-next/profile.yaml +4 -1
- package/profiles/designer-medusa-vite/profile.yaml +4 -1
- package/profiles/docs-writer/profile.yaml +3 -1
- package/profiles/eu-tender-research/README.md +48 -0
- package/profiles/eu-tender-research/logo.png +0 -0
- package/profiles/eu-tender-research/profile.yaml +108 -0
- package/profiles/finance/logo.png +0 -0
- package/profiles/finance/profile.yaml +46 -0
- package/profiles/frontend/profile.yaml +5 -9
- package/profiles/growth/profile.yaml +2 -3
- package/profiles/gstack/profile.yaml +15 -0
- package/profiles/higgsfield/profile.yaml +3 -0
- package/profiles/hyperframes/logo.png +0 -0
- package/profiles/hyperframes/profile.yaml +59 -0
- package/profiles/improver/profile.yaml +88 -0
- package/profiles/marketing/profile.yaml +0 -3
- package/profiles/medusa-dev/profile.yaml +2 -0
- package/profiles/medusa-next/profile.yaml +2 -3
- package/profiles/medusa-vite/profile.yaml +2 -3
- package/profiles/n8n/logo.png +0 -0
- package/profiles/n8n/profile.yaml +50 -0
- package/profiles/nextjs/profile.yaml +2 -3
- package/profiles/ops/profile.yaml +2 -0
- package/profiles/postizz/profile.yaml +13 -3
- package/profiles/python/profile.yaml +3 -0
- package/profiles/research/profile.yaml +3 -1
- package/profiles/schema.json +10 -0
- package/profiles/secops/profile.yaml +2 -0
- package/profiles/seo/profile.yaml +56 -0
- package/profiles/skill-writer/profile.yaml +8 -0
- package/profiles/ssh/profile.yaml +32 -0
- package/profiles/strapi/logo.png +0 -0
- package/profiles/strapi/profile.yaml +45 -0
- package/profiles/stripe/logo.png +0 -0
- package/profiles/stripe/profile.yaml +1 -0
- package/profiles/supabase/logo.png +0 -0
- package/profiles/supabase/profile.yaml +85 -0
- package/profiles/vercel/logo.png +0 -0
- package/profiles/vercel/profile.yaml +25 -1
- package/profiles/vite/profile.yaml +4 -3
- package/profiles/web-frontend-base/profile.yaml +5 -4
- package/profiles/webshop/profile.yaml +23 -5
- package/profiles/x-growth-bot/profile.yaml +44 -0
- package/resources/icons/generate-icons.py +128 -2
- package/resources/mcps/configs/claude.sanitized.json +42 -0
- package/resources/mcps/configs/codex.sanitized.json +7 -0
- package/resources/skills/skills/career/resume-version-manager/SKILL.md +351 -0
- package/resources/skills/skills/career/salary-negotiation-prep/SKILL.md +378 -0
- package/resources/skills/skills/content/pdf/SKILL.md +2 -0
- package/resources/skills/skills/content/postiz-cards/SKILL.md +48 -0
- package/resources/skills/skills/content/postiz-cards/scripts/analytics.sh +38 -0
- package/resources/skills/skills/content/postiz-cards/scripts/card.sh +42 -0
- package/resources/skills/skills/content/postiz-cards/scripts/lint.py +38 -0
- package/resources/skills/skills/design/headless-gif-demo/SKILL.md +1 -1
- package/resources/skills/skills/design/readme-svg-design/SKILL.md +1 -1
- package/resources/skills/skills/eu-funding/grant-outreach/SKILL.md +70 -0
- package/resources/skills/skills/eu-funding/hu-grant-finder/SKILL.md +114 -0
- package/resources/skills/skills/eu-funding/hu-grant-finder/evals.md +26 -0
- package/resources/skills/skills/eu-funding/ted-tender-search/SKILL.md +80 -0
- package/resources/skills/skills/eu-funding/ted-tender-search/evals.md +26 -0
- package/resources/skills/skills/eu-funding/ted-tender-search/scripts/ted-search.sh +46 -0
- package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +1 -1
- package/resources/skills/skills/github/gx-agents/SKILL.md +96 -0
- package/resources/skills/skills/gstack/design-shotgun/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ab-test-analyzer/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ab-test-setup-and-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/account-structure-review/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ad-copy-variant-generator/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ad-extension-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ad-spend-allocator/SKILL.md +1 -1
- package/resources/skills/skills/marketing/anomaly-detection/SKILL.md +1 -1
- package/resources/skills/skills/marketing/attribution-model-comparison/SKILL.md +1 -1
- package/resources/skills/skills/marketing/audience-overlap-analysis/SKILL.md +7 -1
- package/resources/skills/skills/marketing/bid-strategy-recommendations/SKILL.md +7 -1
- package/resources/skills/skills/marketing/budget-scenario-planner/SKILL.md +6 -1
- package/resources/skills/skills/marketing/campaign-naming-convention-builder/SKILL.md +7 -1
- package/resources/skills/skills/marketing/channel-mix-optimizer/SKILL.md +7 -1
- package/resources/skills/skills/marketing/client-report-narratives/SKILL.md +6 -1
- package/resources/skills/skills/marketing/competitor-creative-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/competitor-teardown/SKILL.md +1 -1
- package/resources/skills/skills/marketing/content-repurposer/SKILL.md +1 -1
- package/resources/skills/skills/marketing/conversion-path-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/cpa-diagnostics/SKILL.md +1 -1
- package/resources/skills/skills/marketing/creative-fatigue-detection/SKILL.md +1 -1
- package/resources/skills/skills/marketing/day-hour-performance-breakdown/SKILL.md +1 -1
- package/resources/skills/skills/marketing/device-performance-split/SKILL.md +1 -1
- package/resources/skills/skills/marketing/e2e-seo-assistant/SKILL.md +1 -1
- package/resources/skills/skills/marketing/email-sequence-writer/SKILL.md +1 -1
- package/resources/skills/skills/marketing/frequency-cap-recommendations/SKILL.md +1 -1
- package/resources/skills/skills/marketing/geo-performance-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/google-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/icp-research-assistant/SKILL.md +1 -1
- package/resources/skills/skills/marketing/keyword-cannibalization-check/SKILL.md +1 -1
- package/resources/skills/skills/marketing/landing-page-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/landing-page-audit-quick/SKILL.md +1 -1
- package/resources/skills/skills/marketing/linkedin-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/meta-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/pacing-monitor/SKILL.md +1 -1
- package/resources/skills/skills/marketing/performance-benchmarking/SKILL.md +1 -1
- package/resources/skills/skills/marketing/programmatic-seo-builder/SKILL.md +1 -1
- package/resources/skills/skills/marketing/quality-score-breakdown/SKILL.md +1 -1
- package/resources/skills/skills/marketing/reddit-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/retargeting-window-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/roas-forecasting/SKILL.md +1 -1
- package/resources/skills/skills/marketing/search-term-mining/SKILL.md +1 -1
- package/resources/skills/skills/marketing/utm-tracking-generator/SKILL.md +1 -1
- package/resources/skills/skills/marketing/wasted-spend-finder/SKILL.md +1 -1
- package/resources/skills/skills/marketing/weekly-account-summary/SKILL.md +1 -1
- package/resources/skills/skills/meta/awesome-list-submit/SKILL.md +4 -4
- package/resources/skills/skills/meta/cue-dashboard/SKILL.md +109 -0
- package/resources/skills/skills/meta/cue-developer/SKILL.md +161 -0
- package/resources/skills/skills/meta/cue-developer/evals/evals.json +57 -0
- package/resources/skills/skills/meta/cue-developer/references/architecture.md +65 -0
- package/resources/skills/skills/meta/cue-developer/references/build_and_test.md +72 -0
- package/resources/skills/skills/meta/cue-developer/references/contributing.md +75 -0
- package/resources/skills/skills/meta/cue-developer/references/conventions.md +57 -0
- package/resources/skills/skills/meta/cue-developer/references/first_time_setup.md +51 -0
- package/resources/skills/skills/meta/cue-developer/references/skill_and_mcp_authoring.md +84 -0
- package/resources/skills/skills/meta/cue-developer/references/troubleshooting.md +42 -0
- package/resources/skills/skills/meta/delegation-check/SKILL.md +148 -0
- package/resources/skills/skills/meta/delegation-check/specs/scan-algorithm.md +125 -0
- package/resources/skills/skills/meta/delegation-check/specs/separation-rules.md +190 -0
- package/resources/skills/skills/meta/focus/SKILL.md +62 -0
- package/resources/skills/skills/meta/help/SKILL.md +1 -1
- package/resources/skills/skills/meta/integrity-tags/SKILL.md +2 -0
- package/resources/skills/skills/meta/next-steps/SKILL.md +124 -0
- package/resources/skills/skills/meta/next-steps/evals/eval-set.json +92 -0
- package/resources/skills/skills/meta/profile-from-docs/SKILL.md +141 -0
- package/resources/skills/skills/meta/ralph-loop/SKILL.md +83 -0
- package/resources/skills/skills/meta/ralph-loop/scripts/loop.sh +73 -0
- package/resources/skills/skills/meta/skill-simplify/SKILL.md +136 -0
- package/resources/skills/skills/meta/skill-simplify/phases/01-analysis.md +173 -0
- package/resources/skills/skills/meta/skill-simplify/phases/02-optimize.md +104 -0
- package/resources/skills/skills/meta/skill-simplify/phases/03-check.md +145 -0
- package/resources/skills/skills/meta/smart-loader/scripts/smart-lookup.sh +13 -4
- package/resources/skills/skills/meta/verify-council/SKILL.md +182 -0
- package/resources/skills/skills/meta/verify-council/references/lane-prompts.md +103 -0
- package/resources/skills/skills/meta/verify-council/references/workflow.js +217 -0
- package/resources/skills/skills/nvidia/aiq-research/SKILL.md +1 -1
- package/resources/skills/skills/nvidia/cuopt-developer/SKILL.md +16 -1
- package/resources/skills/skills/nvidia/cuopt-developer/resources/contributing.md +2 -2
- package/resources/skills/skills/nvidia/cuopt-developer/resources/numerical_debugging.md +128 -0
- package/resources/skills/skills/nvidia/cuopt-developer/resources/python_bindings.md +2 -9
- package/resources/skills/skills/nvidia/cuopt-developer/resources/vrp_skills.md +166 -0
- package/resources/skills/skills/nvidia/cuopt-install/SKILL.md +2 -10
- package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/SKILL.md +3 -23
- package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/resources/examples.md +40 -20
- package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-python/SKILL.md +5 -1
- package/resources/skills/skills/nvidia/skill-evolution/SKILL.md +4 -5
- package/resources/skills/skills/research/trendradar/SKILL.md +1 -1
- package/resources/skills/skills/ssh/ssh-config/SKILL.md +94 -0
- package/resources/skills/skills/ssh/ssh-copy/SKILL.md +92 -0
- package/resources/skills/skills/ssh/ssh-harden/SKILL.md +108 -0
- package/resources/skills/skills/ssh/ssh-keys/SKILL.md +82 -0
- package/resources/skills/skills/ssh/ssh-paste-image/LICENSE +28 -0
- package/resources/skills/skills/ssh/ssh-paste-image/SKILL.md +149 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/build.sh +29 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/go.mod +3 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/main.go +79 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/ccimgd.service +12 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/com.ccimgd.plist +20 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/go.mod +3 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/main.go +98 -0
- package/resources/skills/skills/ssh/ssh-tunnel/SKILL.md +96 -0
- package/resources/skills/skills/strapi/building-with-strapi/SKILL.md +112 -0
- package/resources/skills/skills/strapi/strapi-cli/SKILL.md +93 -0
- package/resources/skills/skills/strapi/strapi-content-api/SKILL.md +115 -0
- package/resources/skills/skills/strapi/strapi-deploy/SKILL.md +89 -0
- package/resources/skills/skills/strapi/strapi-mcp-setup/SKILL.md +101 -0
- package/resources/skills/skills/strapi/strapi-plugins/SKILL.md +97 -0
- package/resources/skills/skills/tools/context7/SKILL.md +101 -0
- package/resources/skills/skills/tools/opensrc/SKILL.md +1 -1
- package/resources/skills/skills/tools/portless/SKILL.md +186 -0
- package/resources/skills/skills/xbot/operate/SKILL.md +229 -0
- package/src/commands/_index.ts +8 -0
- package/src/commands/ai-score.e2e.test.ts +11 -4
- package/src/commands/ai.ts +3 -4
- package/src/commands/auto-detect.ts +1 -1
- package/src/commands/cli.test.ts +1 -2
- package/src/commands/cli.ts +1 -1
- package/src/commands/cloud.ts +1 -1
- package/src/commands/current.ts +1 -4
- package/src/commands/dash.test.ts +110 -0
- package/src/commands/dash.ts +194 -0
- package/src/commands/dashboard.ts +26 -0
- package/src/commands/diff.ts +1 -1
- package/src/commands/discover.test.ts +1 -1
- package/src/commands/discover.ts +90 -40
- package/src/commands/doctor.test.ts +58 -0
- package/src/commands/doctor.ts +79 -3
- package/src/commands/eval-behavior.ts +1 -1
- package/src/commands/eval.ts +2 -2
- package/src/commands/evolve.ts +4 -3
- package/src/commands/failures.test.ts +1 -1
- package/src/commands/features-batch1.test.ts +6 -1
- package/src/commands/icon.ts +1 -5
- package/src/commands/import-profile.ts +1 -1
- package/src/commands/init.ts +50 -7
- package/src/commands/install-sh.e2e.test.ts +65 -0
- package/src/commands/launch-handoff.e2e.test.ts +88 -0
- package/src/commands/launch.e2e.test.ts +8 -1
- package/src/commands/launch.test.ts +29 -0
- package/src/commands/launch.ts +185 -131
- package/src/commands/lock.ts +0 -1
- package/src/commands/marketplace.ts +0 -4
- package/src/commands/materialize.ts +1 -1
- package/src/commands/mem.ts +341 -0
- package/src/commands/optimizer.ts +0 -3
- package/src/commands/playground.ts +1 -2
- package/src/commands/profile-draft-skill.ts +1 -1
- package/src/commands/replay-whatif.ts +1 -6
- package/src/commands/score.ts +2 -2
- package/src/commands/security.test.ts +88 -0
- package/src/commands/security.ts +74 -28
- package/src/commands/shell.test.ts +65 -4
- package/src/commands/shell.ts +67 -7
- package/src/commands/skills-test.ts +0 -1
- package/src/commands/skills.ts +28 -2
- package/src/commands/sources.ts +1 -2
- package/src/commands/status.ts +2 -6
- package/src/commands/submit-profile.ts +1 -1
- package/src/commands/suggest.ts +35 -10
- package/src/commands/trigger-gaps.test.ts +50 -0
- package/src/commands/trigger-gaps.ts +63 -29
- package/src/commands/update.ts +1 -1
- package/src/commands/validate.ts +16 -4
- package/src/commands/watch-live.ts +1 -1
- package/src/commands/workspace.ts +1 -1
- package/src/index.ts +26 -10
- package/src/lib/active-sessions.ts +1 -1
- package/src/lib/agent-adapters.test.ts +100 -0
- package/src/lib/agent-adapters.ts +2 -2
- package/src/lib/analytics.test.ts +88 -0
- package/src/lib/analytics.ts +82 -1
- package/src/lib/auto-detect.test.ts +10 -4
- package/src/lib/auto-detect.ts +19 -23
- package/src/lib/brand-icons.ts +0 -1
- package/src/lib/cache.ts +2 -3
- package/src/lib/claude-mem-env.test.ts +148 -0
- package/src/lib/claude-mem-env.ts +172 -0
- package/src/lib/combo-history.test.ts +53 -0
- package/src/lib/combo-history.ts +83 -0
- package/src/lib/companion-detect.test.ts +108 -0
- package/src/lib/companion-detect.ts +140 -0
- package/src/lib/companion-fetch.ts +4 -6
- package/src/lib/conditional-skills.test.ts +1 -1
- package/src/lib/config-paths.test.ts +53 -0
- package/src/lib/config-paths.ts +33 -0
- package/src/lib/dashboard-server.test.ts +351 -0
- package/src/lib/dashboard-server.ts +1476 -27
- package/src/lib/debug-log.test.ts +66 -0
- package/src/lib/debug-log.ts +45 -0
- package/src/lib/mcp-catalog.test.ts +102 -0
- package/src/lib/mcp-catalog.ts +193 -0
- package/src/lib/pair-suggestions.test.ts +111 -0
- package/src/lib/pair-suggestions.ts +98 -5
- package/src/lib/permissions.test.ts +76 -0
- package/src/lib/permissions.ts +125 -0
- package/src/lib/picker.test.ts +1106 -1
- package/src/lib/picker.ts +1230 -142
- package/src/lib/plugin-discovery.ts +126 -0
- package/src/lib/pr-poster.ts +1 -1
- package/src/lib/pr-throttle.ts +2 -6
- package/src/lib/profile-linter.test.ts +67 -1
- package/src/lib/profile-linter.ts +59 -14
- package/src/lib/profile-loader.test.ts +21 -0
- package/src/lib/profile-loader.ts +22 -3
- package/src/lib/profile-metrics.ts +2 -6
- package/src/lib/profile-names.test.ts +58 -0
- package/src/lib/repos.test.ts +57 -0
- package/src/lib/repos.ts +167 -0
- package/src/lib/resolver-npx.ts +10 -1
- package/src/lib/runtime-materializer.test.ts +200 -3
- package/src/lib/runtime-materializer.ts +129 -20
- package/src/lib/shared-profiles.ts +2 -3
- package/src/lib/skill-clis.test.ts +113 -0
- package/src/lib/skill-clis.ts +232 -0
- package/src/lib/skill-dependencies.ts +9 -1
- package/src/lib/skill-deps.ts +1 -1
- package/src/lib/skill-linter.ts +1 -1
- package/src/lib/skill-quality.ts +0 -1
- package/src/lib/skill-sandbox.test.ts +1 -1
- package/src/lib/skills-lock.test.ts +1 -1
- package/src/lib/telemetry-consent.ts +3 -5
- package/src/lib/telemetry-report.test.ts +2 -2
- package/src/lib/token-budget.ts +111 -0
- package/src/lib/trigger-gaps.test.ts +70 -0
- package/src/lib/trigger-gaps.ts +48 -6
- package/src/lib/tui/data.ts +1 -5
- package/src/lib/workflow-store.ts +150 -0
- package/src/lib/workspace-secrets.ts +0 -4
- package/src/lib/workspaces.ts +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI extraction from SKILL.md frontmatter, the data behind the
|
|
3
|
+
* studio's "CLIs" tab. Recipe enrichment runs against the real on-disk
|
|
4
|
+
* cli-recipes.json.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
extractClisFromContent,
|
|
11
|
+
cliInstallHint,
|
|
12
|
+
hasRecipe,
|
|
13
|
+
aggregateProfileClis,
|
|
14
|
+
} from "./skill-clis";
|
|
15
|
+
|
|
16
|
+
const SKILL = (allowed: string) =>
|
|
17
|
+
`---\nname: demo\nallowed-tools: ${allowed}\n---\n\n# demo\n\nBody mentions Bash(should-not-leak:*) outside frontmatter.\n`;
|
|
18
|
+
|
|
19
|
+
describe("extractClisFromContent", () => {
|
|
20
|
+
test("pulls tool names out of Bash(...) frontmatter refs", () => {
|
|
21
|
+
const clis = extractClisFromContent(SKILL("Bash(cargo:*), Bash(gh:*), Bash(curl:*)"));
|
|
22
|
+
expect(clis).toEqual(["cargo", "gh", "curl"]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("drops shell builtins, coreutils, and Claude tool names", () => {
|
|
26
|
+
const clis = extractClisFromContent(SKILL("Bash(Bash:*), Bash(Read:*), Bash(-:*), Bash(ls:*), Bash(grep:*), Bash(cargo:*)"));
|
|
27
|
+
expect(clis).toEqual(["cargo"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("takes the first token of a multi-word command", () => {
|
|
31
|
+
const clis = extractClisFromContent(SKILL("Bash(npx medusa db:generate:*)"));
|
|
32
|
+
expect(clis).toEqual(["npx"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("dedupes repeated tools, preserves first-seen casing", () => {
|
|
36
|
+
const clis = extractClisFromContent(SKILL("Bash(Cargo:*), Bash(cargo:*)"));
|
|
37
|
+
expect(clis).toEqual(["Cargo"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("ignores Bash(...) that appears only in the body", () => {
|
|
41
|
+
const noFront = "# demo\n\nrun Bash(docker:*) here\n";
|
|
42
|
+
expect(extractClisFromContent(noFront)).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns [] when there's no allowed-tools", () => {
|
|
46
|
+
expect(extractClisFromContent("---\nname: demo\n---\n\n# demo\n")).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("extractClisFromContent — Prerequisites + body sources", () => {
|
|
51
|
+
test("picks up a recipe tool from a ## Prerequisites table, even without frontmatter", () => {
|
|
52
|
+
const md = "---\nname: demo\n---\n\n# demo\n\n## Prerequisites\n\n| Tool | Install |\n|---|---|\n| `nmap` | `apt install nmap` |\n\n## Steps\n";
|
|
53
|
+
expect(extractClisFromContent(md)).toContain("nmap");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("ignores non-recipe backticked tokens in Prerequisites (precision)", () => {
|
|
57
|
+
const md = "---\nname: demo\n---\n\n## Prerequisites\n\n- `totally-made-up-tool` — install somehow\n";
|
|
58
|
+
expect(extractClisFromContent(md)).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("catches a recipe tool used in command position in the body", () => {
|
|
62
|
+
const md = "---\nname: demo\n---\n\n# demo\n\nRun the suite:\n\n```bash\n$ cargo nextest run\n```\n";
|
|
63
|
+
expect(extractClisFromContent(md)).toContain("cargo");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("does NOT catch a recipe tool name used as prose (precision)", () => {
|
|
67
|
+
const md = "---\nname: demo\n---\n\n# demo\n\nWe use cargo culting sparingly and just move on.\n";
|
|
68
|
+
// "cargo" mid-sentence and "just" mid-sentence are prose, not commands.
|
|
69
|
+
expect(extractClisFromContent(md)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("prefers the longest recipe key at a position (cargo-nextest over cargo)", () => {
|
|
73
|
+
const md = "---\nname: demo\n---\n\n```\n$ cargo-nextest run\n```\n";
|
|
74
|
+
const clis = extractClisFromContent(md);
|
|
75
|
+
expect(clis).toContain("cargo-nextest");
|
|
76
|
+
expect(clis).not.toContain("cargo");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("cli-recipes enrichment", () => {
|
|
81
|
+
test("known tools resolve an install hint", () => {
|
|
82
|
+
// nmap is a stable entry in cli-recipes.json (apt/brew/...).
|
|
83
|
+
expect(hasRecipe("nmap")).toBe(true);
|
|
84
|
+
expect(cliInstallHint("nmap")).toMatch(/nmap/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("unknown tools have no recipe and an empty hint", () => {
|
|
88
|
+
expect(hasRecipe("definitely-not-a-cli-xyz")).toBe(false);
|
|
89
|
+
expect(cliInstallHint("definitely-not-a-cli-xyz")).toBe("");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("aggregateProfileClis", () => {
|
|
94
|
+
test("unions across skills, ranks by usage, records usedBy", () => {
|
|
95
|
+
const skills = [
|
|
96
|
+
{ id: "rust/a", body: SKILL("Bash(cargo:*), Bash(rustup:*)") },
|
|
97
|
+
{ id: "rust/b", body: SKILL("Bash(cargo:*)") },
|
|
98
|
+
{ id: "ops/c", body: SKILL("Bash(gh:*)") },
|
|
99
|
+
];
|
|
100
|
+
const clis = aggregateProfileClis(skills);
|
|
101
|
+
// cargo (2 users) ranks first
|
|
102
|
+
expect(clis[0]!.name).toBe("cargo");
|
|
103
|
+
expect(clis[0]!.usedBy).toEqual(["rust/a", "rust/b"]);
|
|
104
|
+
expect(clis.map((c) => c.name).sort()).toEqual(["cargo", "gh", "rustup"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("skips missing (unresolved) skills", () => {
|
|
108
|
+
const clis = aggregateProfileClis([
|
|
109
|
+
{ id: "x", body: SKILL("Bash(cargo:*)"), missing: true },
|
|
110
|
+
]);
|
|
111
|
+
expect(clis).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the external CLI tools a skill depends on, for the studio's "CLIs"
|
|
3
|
+
* tab. The high-fidelity signal is a SKILL.md's frontmatter `allowed-tools:`
|
|
4
|
+
* list — `Bash(<tool>:*)` entries name exactly what the skill shells out to.
|
|
5
|
+
* We parse those, drop shell builtins / coreutils / Claude's own tool names,
|
|
6
|
+
* and enrich each surviving tool with an install hint from
|
|
7
|
+
* `resources/cli-recipes.json` when one exists.
|
|
8
|
+
*
|
|
9
|
+
* Paths resolve per-call from env so tests can point CUE_REPO_ROOT at a
|
|
10
|
+
* fixture without touching the real recipes file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { resolve, dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
function repoRoot(): string {
|
|
18
|
+
return (
|
|
19
|
+
process.env.CUE_REPO_ROOT ??
|
|
20
|
+
process.env.SOUL_REPO_ROOT ??
|
|
21
|
+
resolve(dirname(fileURLToPath(import.meta.url)), "..", "..")
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function recipesPath(): string {
|
|
25
|
+
return join(repoRoot(), "resources", "cli-recipes.json");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProfileCli {
|
|
29
|
+
/** Tool name as declared in `Bash(<name>…)`. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Best-effort install command from cli-recipes.json, or "" if no recipe. */
|
|
32
|
+
install: string;
|
|
33
|
+
/** True when cli-recipes.json has a recipe for this tool. */
|
|
34
|
+
known: boolean;
|
|
35
|
+
/** Skill ids (e.g. "rust/cargo-nextest") that declare this CLI. */
|
|
36
|
+
usedBy: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Not external CLIs: Claude's own tool names + shell builtins / coreutils that
|
|
40
|
+
// every box already has. Dropping these keeps the tab to real dependencies.
|
|
41
|
+
const NON_CLI = new Set([
|
|
42
|
+
"-", "bash", "read", "write", "edit", "multiedit", "notebookedit", "glob",
|
|
43
|
+
"grep", "task", "webfetch", "websearch", "todowrite", "ls", "cat", "head",
|
|
44
|
+
"tail", "find", "echo", "cd", "mkdir", "rmdir", "rm", "cp", "mv", "sed",
|
|
45
|
+
"awk", "sort", "uniq", "wc", "cut", "tr", "xargs", "test", "sleep", "env",
|
|
46
|
+
"export", "source", "pwd", "touch", "chmod", "chown", "dirname", "basename",
|
|
47
|
+
"true", "false", "set", "printf", "tee", "comm", "diff", "date", "seq",
|
|
48
|
+
"tput", "which", "type", "eval", "exit", "return", "local", "for", "while",
|
|
49
|
+
"if", "then", "fi", "do", "done", "case", "esac",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/** Split a SKILL.md into its frontmatter block and body. */
|
|
53
|
+
function splitFrontmatter(content: string): { front: string; body: string } {
|
|
54
|
+
const lines = content.split("\n");
|
|
55
|
+
if (lines[0] !== "---") return { front: "", body: content };
|
|
56
|
+
for (let i = 1; i < lines.length; i++) {
|
|
57
|
+
if (lines[i] === "---") return { front: lines.slice(1, i).join("\n"), body: lines.slice(i + 1).join("\n") };
|
|
58
|
+
}
|
|
59
|
+
return { front: "", body: content };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Source A — `Bash(<tool>…)` refs in the `allowed-tools:` frontmatter. */
|
|
63
|
+
function clisFromFrontmatter(front: string, out: Map<string, string>): void {
|
|
64
|
+
const re = /Bash\(\s*([^):*\s]+)/g;
|
|
65
|
+
let m: RegExpExecArray | null;
|
|
66
|
+
while ((m = re.exec(front)) !== null) {
|
|
67
|
+
const raw = m[1]!.trim();
|
|
68
|
+
const key = raw.toLowerCase();
|
|
69
|
+
if (!raw || NON_CLI.has(key)) continue;
|
|
70
|
+
if (!out.has(key)) out.set(key, raw);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Pull the `## Prerequisites` section body, or "" if the skill has none. */
|
|
75
|
+
function prerequisitesSection(body: string): string {
|
|
76
|
+
const lines = body.split("\n");
|
|
77
|
+
const start = lines.findIndex((l) => /^#{1,6}\s+Prerequisites\b/i.test(l));
|
|
78
|
+
if (start < 0) return "";
|
|
79
|
+
let end = lines.length;
|
|
80
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
81
|
+
if (/^#{1,6}\s+/.test(lines[i]!)) { end = i; break; }
|
|
82
|
+
}
|
|
83
|
+
return lines.slice(start + 1, end).join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Source B — backticked tool names in `## Prerequisites`, recipe-bounded. */
|
|
87
|
+
function clisFromPrerequisites(body: string, out: Map<string, string>): void {
|
|
88
|
+
const section = prerequisitesSection(body);
|
|
89
|
+
if (!section) return;
|
|
90
|
+
const re = /`([a-zA-Z][a-zA-Z0-9._-]*)`/g;
|
|
91
|
+
let m: RegExpExecArray | null;
|
|
92
|
+
while ((m = re.exec(section)) !== null) {
|
|
93
|
+
const raw = m[1]!.trim();
|
|
94
|
+
const key = raw.toLowerCase();
|
|
95
|
+
if (NON_CLI.has(key)) continue;
|
|
96
|
+
// Precision gate: only accept Prerequisites tokens that are real recipes.
|
|
97
|
+
if (!hasRecipe(raw)) continue;
|
|
98
|
+
if (!out.has(key)) out.set(key, raw);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let bodyScanCache: { root: string; re: RegExp | null } | null = null;
|
|
103
|
+
/**
|
|
104
|
+
* A single command-position regex over all recipe keys, longest-first so
|
|
105
|
+
* `cargo-nextest` wins over `cargo`. "Command position" = start of line, or
|
|
106
|
+
* right after a backtick / `$` / pipe / `>` (optionally a `$ ` prompt). This
|
|
107
|
+
* keeps prose mentions ("use cargo wisely") out while catching real command
|
|
108
|
+
* usage ("`cargo nextest`", "$ nmap -sV").
|
|
109
|
+
*/
|
|
110
|
+
function bodyScanRegex(): RegExp | null {
|
|
111
|
+
const root = repoRoot();
|
|
112
|
+
if (bodyScanCache && bodyScanCache.root === root) return bodyScanCache.re;
|
|
113
|
+
const keys = Object.keys(loadRecipes()).sort((a, b) => b.length - a.length);
|
|
114
|
+
const re = keys.length
|
|
115
|
+
? new RegExp(`(?:^|[\\n\`$|>])[ \\t]*(?:\\$[ \\t]*)?(${keys.map(escapeRe).join("|")})(?![\\w-])`, "gm")
|
|
116
|
+
: null;
|
|
117
|
+
bodyScanCache = { root, re };
|
|
118
|
+
return re;
|
|
119
|
+
}
|
|
120
|
+
function escapeRe(s: string): string {
|
|
121
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Concatenate only the code spans of a markdown body — fenced blocks (``` or
|
|
126
|
+
* ~~~) and inline `code` — each on its own line. Restricting the body scan to
|
|
127
|
+
* code keeps English-ambiguous recipe keys (`just`, `cross`, `uv`) from
|
|
128
|
+
* matching prose: a tool in code is a command, a word in a sentence is not.
|
|
129
|
+
*/
|
|
130
|
+
function codeRegions(body: string): string {
|
|
131
|
+
const out: string[] = [];
|
|
132
|
+
const fence = /(?:```|~~~)[^\n]*\n([\s\S]*?)(?:```|~~~)/g;
|
|
133
|
+
let m: RegExpExecArray | null;
|
|
134
|
+
let stripped = body;
|
|
135
|
+
while ((m = fence.exec(body)) !== null) out.push(m[1]!);
|
|
136
|
+
// Remove fenced blocks before scanning inline code so their backticks don't
|
|
137
|
+
// confuse the inline matcher.
|
|
138
|
+
stripped = body.replace(fence, "\n");
|
|
139
|
+
const inline = /`([^`\n]+)`/g;
|
|
140
|
+
while ((m = inline.exec(stripped)) !== null) out.push(m[1]!);
|
|
141
|
+
return out.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Source C — recipe-key tools used in command position inside code spans. */
|
|
145
|
+
function clisFromBody(body: string, out: Map<string, string>): void {
|
|
146
|
+
const re = bodyScanRegex();
|
|
147
|
+
if (!re) return;
|
|
148
|
+
const code = codeRegions(body);
|
|
149
|
+
if (!code) return;
|
|
150
|
+
re.lastIndex = 0;
|
|
151
|
+
let m: RegExpExecArray | null;
|
|
152
|
+
while ((m = re.exec(code)) !== null) {
|
|
153
|
+
const raw = m[1]!;
|
|
154
|
+
const key = raw.toLowerCase();
|
|
155
|
+
if (NON_CLI.has(key)) continue;
|
|
156
|
+
if (!out.has(key)) out.set(key, raw);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* CLI tools a skill depends on, merged from three sources, deduped (first-seen
|
|
162
|
+
* casing preserved), minus shell builtins / Claude tools:
|
|
163
|
+
* A. frontmatter `allowed-tools: Bash(<tool>…)` — explicit, any tool.
|
|
164
|
+
* B. `## Prerequisites` backticked tools — recipe-bounded for precision.
|
|
165
|
+
* C. command-position mentions of cli-recipes.json tools in the body.
|
|
166
|
+
*/
|
|
167
|
+
export function extractClisFromContent(content: string): string[] {
|
|
168
|
+
const { front, body } = splitFrontmatter(content);
|
|
169
|
+
const out = new Map<string, string>();
|
|
170
|
+
clisFromFrontmatter(front, out);
|
|
171
|
+
clisFromPrerequisites(body, out);
|
|
172
|
+
clisFromBody(body, out);
|
|
173
|
+
return [...out.values()];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let recipesCache: { root: string; recipes: Record<string, Record<string, string>> } | null = null;
|
|
177
|
+
function loadRecipes(): Record<string, Record<string, string>> {
|
|
178
|
+
const root = repoRoot();
|
|
179
|
+
if (recipesCache && recipesCache.root === root) return recipesCache.recipes;
|
|
180
|
+
let recipes: Record<string, Record<string, string>> = {};
|
|
181
|
+
try {
|
|
182
|
+
const raw = JSON.parse(readFileSync(recipesPath(), "utf8")) as Record<string, unknown>;
|
|
183
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
184
|
+
if (k.startsWith("$")) continue; // skip $schema-comment
|
|
185
|
+
if (v && typeof v === "object") recipes[k] = v as Record<string, string>;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
recipes = {};
|
|
189
|
+
}
|
|
190
|
+
recipesCache = { root, recipes };
|
|
191
|
+
return recipes;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Best-effort one-line install command for a CLI, or "" when unknown. */
|
|
195
|
+
export function cliInstallHint(name: string): string {
|
|
196
|
+
const r = loadRecipes()[name];
|
|
197
|
+
if (!r) return "";
|
|
198
|
+
if (r.apt) return `apt install ${r.apt}`;
|
|
199
|
+
if (r.brew) return `brew install ${r.brew}`;
|
|
200
|
+
if (r.pacman) return `pacman -S ${r.pacman}`;
|
|
201
|
+
if (r.dnf) return `dnf install ${r.dnf}`;
|
|
202
|
+
if (r.pipx) return `pipx install ${r.pipx}`;
|
|
203
|
+
if (r.pip) return `pip install ${r.pip}`;
|
|
204
|
+
if (r.npm) return `npm i -g ${r.npm}`;
|
|
205
|
+
if (r.script) return r.script;
|
|
206
|
+
if (r.manual) return r.manual;
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** True when cli-recipes.json has a recipe for this tool. */
|
|
211
|
+
export function hasRecipe(name: string): boolean {
|
|
212
|
+
return Boolean(loadRecipes()[name]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Aggregate the CLIs across a set of skills into a per-profile list, ranked by
|
|
217
|
+
* how many skills use each (then alphabetical). `skills` carries each skill's
|
|
218
|
+
* id + full SKILL.md body (already loaded by the caller).
|
|
219
|
+
*/
|
|
220
|
+
export function aggregateProfileClis(skills: { id: string; body: string; missing?: boolean }[]): ProfileCli[] {
|
|
221
|
+
const map = new Map<string, string[]>();
|
|
222
|
+
for (const s of skills) {
|
|
223
|
+
if (s.missing) continue;
|
|
224
|
+
for (const cli of extractClisFromContent(s.body)) {
|
|
225
|
+
if (!map.has(cli)) map.set(cli, []);
|
|
226
|
+
map.get(cli)!.push(s.id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return [...map.entries()]
|
|
230
|
+
.map(([name, usedBy]) => ({ name, install: cliInstallHint(name), known: hasRecipe(name), usedBy }))
|
|
231
|
+
.sort((a, b) => b.usedBy.length - a.usedBy.length || a.name.localeCompare(b.name));
|
|
232
|
+
}
|
|
@@ -36,13 +36,21 @@ function parseExplicitDeps(content: string): string[] {
|
|
|
36
36
|
/**
|
|
37
37
|
* Detect implicit MCP dependencies from mcp__<server>__<tool> references.
|
|
38
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* Pseudo-MCP names the *host* provides, not something cue installs. `conductor`
|
|
41
|
+
* is the host orchestrator that exposes tools like
|
|
42
|
+
* `mcp__conductor__AskUserQuestion`; skills reference it only as documentation,
|
|
43
|
+
* so it must never be reported as a missing dependency.
|
|
44
|
+
*/
|
|
45
|
+
const HOST_PROVIDED_MCPS = new Set(["conductor"]);
|
|
46
|
+
|
|
39
47
|
function parseImplicitDeps(content: string): string[] {
|
|
40
48
|
const refs = content.match(/mcp__([a-zA-Z][a-zA-Z0-9_-]*)__/g);
|
|
41
49
|
if (!refs) return [];
|
|
42
50
|
const servers = new Set<string>();
|
|
43
51
|
for (const ref of refs) {
|
|
44
52
|
const match = ref.match(/^mcp__([a-zA-Z][a-zA-Z0-9_-]*)__$/);
|
|
45
|
-
if (match) servers.add(match[1]!);
|
|
53
|
+
if (match && !HOST_PROVIDED_MCPS.has(match[1]!)) servers.add(match[1]!);
|
|
46
54
|
}
|
|
47
55
|
return [...servers];
|
|
48
56
|
}
|
package/src/lib/skill-deps.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* resolution and "why is this skill included?" explanations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, existsSync,
|
|
8
|
+
import { readFileSync, existsSync, } from "node:fs";
|
|
9
9
|
import { join, resolve, dirname } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
|
package/src/lib/skill-linter.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
|
-
import {
|
|
14
|
+
import { resolve, dirname } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
17
17
|
import { parseCLIsFromContent, parseMetadataFromContent } from "../commands/optimizer";
|
package/src/lib/skill-quality.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
6
6
|
import { resolve, dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
-
import { listProfiles, loadProfile } from "./profile-loader";
|
|
10
9
|
|
|
11
10
|
const REPO_ROOT = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
12
11
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
2
|
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { parseAllowedTools, validateToolUsage,
|
|
4
|
+
import { parseAllowedTools, validateToolUsage, generateSandboxReport } from "./skill-sandbox";
|
|
5
5
|
|
|
6
6
|
const TEST_ROOT = join(import.meta.dir, "..", "..", "__test_sandbox__");
|
|
7
7
|
|
|
@@ -17,15 +17,13 @@ import {
|
|
|
17
17
|
unlinkSync,
|
|
18
18
|
writeFileSync,
|
|
19
19
|
} from "node:fs";
|
|
20
|
-
import { homedir } from "node:os";
|
|
21
20
|
import { join } from "node:path";
|
|
21
|
+
import { configDir } from "./config-paths";
|
|
22
22
|
|
|
23
23
|
const CONSENT_RECORD_VERSION = 1;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"), "cue");
|
|
28
|
-
}
|
|
25
|
+
// Re-exported for back-compat: external callers import `configDir` from here.
|
|
26
|
+
export { configDir };
|
|
29
27
|
|
|
30
28
|
export function consentPath(): string {
|
|
31
29
|
return join(configDir(), ".telemetry-consent");
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync,
|
|
2
|
+
import { mkdtempSync, rmSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { enable } from "./telemetry-consent";
|
|
7
7
|
import { recordEvent } from "./analytics";
|
|
8
8
|
import { compositeReport, missLeaderboard, topSkills, zombies } from "./telemetry-report";
|
|
9
9
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-budget accounting for materialized profiles — pure, filesystem-free.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from commands/launch.ts (which re-exports these for back-compat).
|
|
5
|
+
* The *formatting* of the budget (colors, the CLI banner block) stays in
|
|
6
|
+
* launch.ts; this module is just the measurement math, so it's unit-testable
|
|
7
|
+
* in isolation and reusable by other surfaces (status, doctor, dashboard).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ResolvedProfile } from "../../profiles/_types";
|
|
11
|
+
|
|
12
|
+
export interface SkillTokens {
|
|
13
|
+
/** Tokens for the YAML frontmatter (always-on, loaded into skill router). */
|
|
14
|
+
frontmatter: number;
|
|
15
|
+
/** Tokens for the rest of SKILL.md (load-on-activate). */
|
|
16
|
+
body: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TokenBreakdown {
|
|
20
|
+
/** Sum of frontmatter tokens across every skill — the real always-on cost. */
|
|
21
|
+
alwaysOn: number;
|
|
22
|
+
/** Sum of body tokens — the ceiling if every skill activates this session. */
|
|
23
|
+
maxIfAllActivate: number;
|
|
24
|
+
/** Skill count for the header line. */
|
|
25
|
+
totalSkills: number;
|
|
26
|
+
/**
|
|
27
|
+
* Per-profile attribution of `alwaysOn` for composite selectors (length > 1).
|
|
28
|
+
* Each skill is credited to the first part that declares it, so per-part
|
|
29
|
+
* numbers sum to `alwaysOn` (no double-counting from overlap). Empty for
|
|
30
|
+
* single-part profiles. `icon` carries the part's emoji when declared.
|
|
31
|
+
*/
|
|
32
|
+
byProfile: { name: string; icon?: string; tokens: number; skillCount: number }[];
|
|
33
|
+
/** Skills sorted by body size, descending — for the "heaviest if activated" hint. */
|
|
34
|
+
heaviestBodies: { id: string; tokens: number }[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function computeTokenBreakdown(
|
|
38
|
+
profile: ResolvedProfile,
|
|
39
|
+
parts: ResolvedProfile[] | undefined,
|
|
40
|
+
tokensForSkill: (id: string) => SkillTokens,
|
|
41
|
+
): TokenBreakdown {
|
|
42
|
+
let alwaysOn = 0;
|
|
43
|
+
let maxIfAllActivate = 0;
|
|
44
|
+
const heaviestBodies: { id: string; tokens: number }[] = [];
|
|
45
|
+
for (const s of profile.skills.local) {
|
|
46
|
+
const { frontmatter, body } = tokensForSkill(s.id);
|
|
47
|
+
alwaysOn += frontmatter;
|
|
48
|
+
maxIfAllActivate += body;
|
|
49
|
+
if (body > 0) heaviestBodies.push({ id: s.id, tokens: body });
|
|
50
|
+
}
|
|
51
|
+
heaviestBodies.sort((a, b) => b.tokens - a.tokens);
|
|
52
|
+
|
|
53
|
+
const byProfile: TokenBreakdown["byProfile"] = [];
|
|
54
|
+
if (parts && parts.length > 1) {
|
|
55
|
+
const credited = new Set<string>();
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
let pTokens = 0;
|
|
58
|
+
let pCount = 0;
|
|
59
|
+
for (const s of part.skills.local) {
|
|
60
|
+
if (credited.has(s.id)) continue;
|
|
61
|
+
credited.add(s.id);
|
|
62
|
+
const { frontmatter } = tokensForSkill(s.id);
|
|
63
|
+
if (frontmatter > 0) {
|
|
64
|
+
pTokens += frontmatter;
|
|
65
|
+
pCount += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
byProfile.push({ name: part.name, icon: part.icon, tokens: pTokens, skillCount: pCount });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
alwaysOn,
|
|
74
|
+
maxIfAllActivate,
|
|
75
|
+
totalSkills: profile.skills.local.length,
|
|
76
|
+
byProfile,
|
|
77
|
+
heaviestBodies,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract frontmatter byte length from a SKILL.md string. Returns
|
|
83
|
+
* `{ frontmatter, body }` byte counts. Falls back to a token count of zero
|
|
84
|
+
* when the file lacks the leading `---` block (still legal but rare).
|
|
85
|
+
*/
|
|
86
|
+
export function splitSkillBytes(source: string): { frontmatter: number; body: number } {
|
|
87
|
+
if (!source.startsWith("---\n") && !source.startsWith("---\r\n")) {
|
|
88
|
+
return { frontmatter: 0, body: source.length };
|
|
89
|
+
}
|
|
90
|
+
// Find the closing `---` on its own line. Search starts after the opener.
|
|
91
|
+
const closer = source.indexOf("\n---", 4);
|
|
92
|
+
if (closer === -1) {
|
|
93
|
+
return { frontmatter: source.length, body: 0 };
|
|
94
|
+
}
|
|
95
|
+
// Include the closing `---\n` in the frontmatter block.
|
|
96
|
+
const fmEnd = source.indexOf("\n", closer + 1);
|
|
97
|
+
const cut = fmEnd === -1 ? source.length : fmEnd + 1;
|
|
98
|
+
return { frontmatter: cut, body: source.length - cut };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Map an always-on token count to the bands we color in the CLI banner and
|
|
103
|
+
* the tmux pane-border badge. Single source of truth so the two displays
|
|
104
|
+
* never drift apart on threshold values.
|
|
105
|
+
*/
|
|
106
|
+
export function tokenLevelEmoji(alwaysOn: number): "🔴" | "🟠" | "🟡" | "🟢" {
|
|
107
|
+
return alwaysOn > 15000 ? "🔴"
|
|
108
|
+
: alwaysOn > 10000 ? "🟠"
|
|
109
|
+
: alwaysOn > 5000 ? "🟡"
|
|
110
|
+
: "🟢";
|
|
111
|
+
}
|
|
@@ -103,6 +103,76 @@ describe("computeTriggerGaps", () => {
|
|
|
103
103
|
expect(rows.map((r) => r.id)).toEqual(["b", "a", "c"]);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
test("word-boundary: single-word trigger does NOT match inside a larger word", () => {
|
|
107
|
+
const skills = [skill("meta/analyze", "analyze", ["analyze"])];
|
|
108
|
+
// "reanalyze" / "analyzer" must not count as the bare word "analyze".
|
|
109
|
+
const prompts = ["please reanalyze the data", "run the analyzer"];
|
|
110
|
+
expect(
|
|
111
|
+
computeTriggerGaps({ skills, userPrompts: prompts, hits: new Map() }),
|
|
112
|
+
).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("word-boundary: 'help' does not match 'helpful' / 'helper'", () => {
|
|
116
|
+
const skills = [skill("meta/help", "help", ["help"])];
|
|
117
|
+
const prompts = ["that was helpful", "ask the helper agent"];
|
|
118
|
+
expect(
|
|
119
|
+
computeTriggerGaps({ skills, userPrompts: prompts, hits: new Map() }),
|
|
120
|
+
).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("weak single-word trigger only counts when it dominates a short prompt", () => {
|
|
124
|
+
const skills = [skill("meta/help", "help", ["help"])];
|
|
125
|
+
const longPrompt =
|
|
126
|
+
"i need some help refactoring this enormous function that spans several files and modules";
|
|
127
|
+
expect(longPrompt.length).toBeGreaterThan(80);
|
|
128
|
+
// Long prompt that merely contains "help" → not an invocation.
|
|
129
|
+
expect(
|
|
130
|
+
computeTriggerGaps({ skills, userPrompts: [longPrompt], hits: new Map() }),
|
|
131
|
+
).toEqual([]);
|
|
132
|
+
// Short, trigger-dominant prompt → genuine.
|
|
133
|
+
const rows = computeTriggerGaps({
|
|
134
|
+
skills, userPrompts: ["help"], hits: new Map(),
|
|
135
|
+
});
|
|
136
|
+
expect(rows[0]?.gap).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("slash-command trigger matches at any prompt length", () => {
|
|
140
|
+
const skills = [skill("caveman/caveman", "caveman", ["/caveman"])];
|
|
141
|
+
const longPrompt =
|
|
142
|
+
"please switch to /caveman mode because this explanation has gotten far too verbose for me";
|
|
143
|
+
expect(longPrompt.length).toBeGreaterThan(80);
|
|
144
|
+
const rows = computeTriggerGaps({
|
|
145
|
+
skills, userPrompts: [longPrompt], hits: new Map(),
|
|
146
|
+
});
|
|
147
|
+
expect(rows[0]?.gap).toBe(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("multi-word trigger matches at any length (word-boundary, not substring)", () => {
|
|
151
|
+
const skills = [skill("caveman/caveman-commit", "caveman-commit", ["commit message"])];
|
|
152
|
+
const longPrompt =
|
|
153
|
+
"after you finish the refactor, write a clear commit message that explains the reasoning behind it";
|
|
154
|
+
expect(longPrompt.length).toBeGreaterThan(80);
|
|
155
|
+
const rows = computeTriggerGaps({
|
|
156
|
+
skills, userPrompts: [longPrompt], hits: new Map(),
|
|
157
|
+
});
|
|
158
|
+
expect(rows[0]?.gap).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("weakTriggerMaxPromptChars is configurable", () => {
|
|
162
|
+
const skills = [skill("meta/help", "help", ["help"])];
|
|
163
|
+
const prompt = "help me please"; // 14 chars
|
|
164
|
+
// Default (80) → counts.
|
|
165
|
+
expect(
|
|
166
|
+
computeTriggerGaps({ skills, userPrompts: [prompt], hits: new Map() })[0]?.gap,
|
|
167
|
+
).toBe(1);
|
|
168
|
+
// Tightened to 4 → the 14-char prompt is now "too long" for a weak trigger.
|
|
169
|
+
expect(
|
|
170
|
+
computeTriggerGaps({
|
|
171
|
+
skills, userPrompts: [prompt], hits: new Map(), weakTriggerMaxPromptChars: 4,
|
|
172
|
+
}),
|
|
173
|
+
).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
106
176
|
test("limit caps the row count", () => {
|
|
107
177
|
const skills = Array.from({ length: 5 }, (_, i) =>
|
|
108
178
|
skill(`s${i}`, `s${i}`, [`trigger phrase ${i}`]),
|