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,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover Claude Code plugins installed on this machine, independent of the
|
|
3
|
+
* active cue profile. Reads Claude Code's real plugin store:
|
|
4
|
+
*
|
|
5
|
+
* <claude-home>/plugins/installed_plugins.json — the install registry
|
|
6
|
+
* <claude-home>/settings.json → enabledPlugins — the on/off map
|
|
7
|
+
*
|
|
8
|
+
* and enriches each entry with its plugin.json description + bundled-skill
|
|
9
|
+
* count when the install path is present on disk.
|
|
10
|
+
*
|
|
11
|
+
* This backs the studio Plugins page's "all installed" view, which is a
|
|
12
|
+
* superset of the profile's declared `plugins:` (that list is a curated
|
|
13
|
+
* subset cue wires into one profile). claude-home defaults to ~/.claude;
|
|
14
|
+
* override with CUE_CLAUDE_HOME for tests or a non-standard install.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
export interface DiscoveredPlugin {
|
|
22
|
+
/** "name@marketplace" — the id Claude Code keys plugins by. */
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
marketplace: string;
|
|
26
|
+
version: string;
|
|
27
|
+
/** settings.enabledPlugins[id] === true */
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
/** appears anywhere in the enabledPlugins map (known to Claude Code). */
|
|
30
|
+
known: boolean;
|
|
31
|
+
installedAt: string | null;
|
|
32
|
+
installPath: string | null;
|
|
33
|
+
/** From the plugin's .claude-plugin/plugin.json, capped. */
|
|
34
|
+
description: string;
|
|
35
|
+
/** Skills the plugin bundles (SKILL.md count under its install path). */
|
|
36
|
+
skills: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function claudeHome(): string {
|
|
40
|
+
return process.env.CUE_CLAUDE_HOME ?? join(homedir(), ".claude");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readJson<T>(path: string): T | null {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Skills live at <installPath>/skills/<name>/SKILL.md in the plugin layout. */
|
|
52
|
+
function countSkills(installPath: string): number {
|
|
53
|
+
const skillsDir = join(installPath, "skills");
|
|
54
|
+
if (!existsSync(skillsDir)) return 0;
|
|
55
|
+
try {
|
|
56
|
+
return readdirSync(skillsDir, { withFileTypes: true }).filter(
|
|
57
|
+
(d) => d.isDirectory() && existsSync(join(skillsDir, d.name, "SKILL.md")),
|
|
58
|
+
).length;
|
|
59
|
+
} catch {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function manifestDescription(installPath: string): string {
|
|
65
|
+
const m = readJson<{ description?: string }>(
|
|
66
|
+
join(installPath, ".claude-plugin", "plugin.json"),
|
|
67
|
+
);
|
|
68
|
+
return (m?.description ?? "").slice(0, 200);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface InstalledEntry {
|
|
72
|
+
version?: string;
|
|
73
|
+
installPath?: string;
|
|
74
|
+
installedAt?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enumerate every plugin Claude Code knows about on this machine, enabled or
|
|
79
|
+
* not. Returns enabled-first, then alphabetical. Missing files degrade to an
|
|
80
|
+
* empty list rather than throwing — the dashboard must render even on a fresh
|
|
81
|
+
* machine with no plugins.
|
|
82
|
+
*/
|
|
83
|
+
export function discoverInstalledPlugins(): DiscoveredPlugin[] {
|
|
84
|
+
const home = claudeHome();
|
|
85
|
+
const installed = readJson<{ plugins: Record<string, InstalledEntry[]> }>(
|
|
86
|
+
join(home, "plugins", "installed_plugins.json"),
|
|
87
|
+
);
|
|
88
|
+
const settings = readJson<{ enabledPlugins?: Record<string, boolean> }>(
|
|
89
|
+
join(home, "settings.json"),
|
|
90
|
+
);
|
|
91
|
+
const enabledMap = settings?.enabledPlugins ?? {};
|
|
92
|
+
const installedMap = installed?.plugins ?? {};
|
|
93
|
+
|
|
94
|
+
// Union: a plugin can be installed-but-not-in-the-map, or in the map but
|
|
95
|
+
// its install cache pruned. Show both so nothing silently disappears.
|
|
96
|
+
const ids = new Set<string>([
|
|
97
|
+
...Object.keys(installedMap),
|
|
98
|
+
...Object.keys(enabledMap),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const out: DiscoveredPlugin[] = [];
|
|
102
|
+
for (const id of ids) {
|
|
103
|
+
const at = id.lastIndexOf("@");
|
|
104
|
+
const name = at > 0 ? id.slice(0, at) : id;
|
|
105
|
+
const marketplace = at > 0 ? id.slice(at + 1) : "";
|
|
106
|
+
const entry = installedMap[id]?.[0];
|
|
107
|
+
const installPath = entry?.installPath ?? null;
|
|
108
|
+
const onDisk = installPath != null && existsSync(installPath);
|
|
109
|
+
out.push({
|
|
110
|
+
id,
|
|
111
|
+
name,
|
|
112
|
+
marketplace,
|
|
113
|
+
version: entry?.version ?? "unknown",
|
|
114
|
+
enabled: enabledMap[id] === true,
|
|
115
|
+
known: id in enabledMap,
|
|
116
|
+
installedAt: entry?.installedAt ?? null,
|
|
117
|
+
installPath,
|
|
118
|
+
description: onDisk ? manifestDescription(installPath) : "",
|
|
119
|
+
skills: onDisk ? countSkills(installPath) : 0,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return out.sort(
|
|
124
|
+
(a, b) => Number(b.enabled) - Number(a.enabled) || a.id.localeCompare(b.id),
|
|
125
|
+
);
|
|
126
|
+
}
|
package/src/lib/pr-poster.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { spawn } from "node:child_process";
|
|
|
23
23
|
import { createHash } from "node:crypto";
|
|
24
24
|
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from "node:fs";
|
|
25
25
|
import { tmpdir } from "node:os";
|
|
26
|
-
import { join,
|
|
26
|
+
import { join, } from "node:path";
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Build a deterministic branch name keyed by today's date + a hash of the
|
package/src/lib/pr-throttle.ts
CHANGED
|
@@ -14,13 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, renameSync } from "node:fs";
|
|
16
16
|
import { join, dirname } from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { cacheDir } from "./config-paths";
|
|
18
18
|
|
|
19
|
-
const DEFAULT_PATH = join(
|
|
20
|
-
process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"),
|
|
21
|
-
"cue",
|
|
22
|
-
"pr-opened.json",
|
|
23
|
-
);
|
|
19
|
+
const DEFAULT_PATH = join(cacheDir(), "pr-opened.json");
|
|
24
20
|
|
|
25
21
|
export const DEFAULT_COOLDOWN_DAYS = 90;
|
|
26
22
|
export const DAILY_CAP = 25;
|
|
@@ -140,9 +140,38 @@ describe("lintProfile", () => {
|
|
|
140
140
|
expect(result.checks.map((check) => check.name)).toContain("MCPs");
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
test("demotes a local-only MCP (source dir present, absent from sanitized registry) to W9, not E3", async () => {
|
|
144
|
+
// An MCP with a source manifest dir but no sanitized-registry entry is a
|
|
145
|
+
// private/local-only server (wired into the user's own ~/.claude.json), not
|
|
146
|
+
// a dangling reference — warn (W9), like W5 demotes an uninstalled plugin.
|
|
147
|
+
await mkdir(join(root, "mcps", "mcps", "envoult"), { recursive: true });
|
|
148
|
+
await writeFile(join(root, "mcps", "mcps", "envoult", "skills.md"), "# envoult\n", "utf8");
|
|
149
|
+
await writeProfile(
|
|
150
|
+
"priv",
|
|
151
|
+
["name: priv", "description: private mcp profile", "mcps: [envoult]", ""].join("\n"),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const result = await lintProfile("priv", opts());
|
|
155
|
+
|
|
156
|
+
expect(rules(result).filter((rule) => rule === "W9")).toHaveLength(1);
|
|
157
|
+
expect(rules(result).filter((rule) => rule === "E3")).toHaveLength(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("still reports E3 for an MCP with neither a registry entry nor a source dir", async () => {
|
|
161
|
+
await writeProfile(
|
|
162
|
+
"dangling",
|
|
163
|
+
["name: dangling", "description: dangling mcp ref", "mcps: [no-such-mcp]", ""].join("\n"),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const result = await lintProfile("dangling", opts());
|
|
167
|
+
|
|
168
|
+
expect(rules(result).filter((rule) => rule === "E3")).toHaveLength(1);
|
|
169
|
+
expect(rules(result).filter((rule) => rule === "W9")).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
143
172
|
test("reports W1, W2, W3, and W4 warnings", async () => {
|
|
144
173
|
const localRefs: string[] = [];
|
|
145
|
-
for (let i = 0; i <
|
|
174
|
+
for (let i = 0; i < 121; i++) {
|
|
146
175
|
const ref = `bulk/skill-${i}`;
|
|
147
176
|
localRefs.push(ref);
|
|
148
177
|
await writeLocalSkill(ref);
|
|
@@ -237,6 +266,43 @@ describe("lintProfile", () => {
|
|
|
237
266
|
expect(messages).toContain("MCP");
|
|
238
267
|
expect(hasLintErrors(result)).toBe(true);
|
|
239
268
|
});
|
|
269
|
+
|
|
270
|
+
test("offline mode skips uncached npx skills instead of erroring", async () => {
|
|
271
|
+
await writeProfile(
|
|
272
|
+
"npxoffline",
|
|
273
|
+
[
|
|
274
|
+
"name: npxoffline",
|
|
275
|
+
"description: npx offline test",
|
|
276
|
+
"skills:",
|
|
277
|
+
" npx:",
|
|
278
|
+
" - repo: anthropics/skills",
|
|
279
|
+
" skills: [ghost-uncached]",
|
|
280
|
+
"",
|
|
281
|
+
].join("\n"),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
let fetched = false;
|
|
285
|
+
const trackingFetch = async () => {
|
|
286
|
+
fetched = true;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Offline (the validate default): an uncached npx skill is a benign skip,
|
|
290
|
+
// not an E3, and the network fetcher is never invoked.
|
|
291
|
+
const offline = await lintProfile(
|
|
292
|
+
"npxoffline",
|
|
293
|
+
opts({ npxOffline: true, npxFetch: trackingFetch }),
|
|
294
|
+
);
|
|
295
|
+
expect(fetched).toBe(false);
|
|
296
|
+
expect(rules(offline).filter((rule) => rule === "E3")).toHaveLength(0);
|
|
297
|
+
const npxCheck = offline.checks.find((check) => check.name === "npx skills");
|
|
298
|
+
expect(npxCheck?.message).toContain("not cached (offline");
|
|
299
|
+
expect(hasLintErrors(offline)).toBe(false);
|
|
300
|
+
|
|
301
|
+
// Online (--online): the fetcher IS invoked, preserving fetchability checks.
|
|
302
|
+
fetched = false;
|
|
303
|
+
await lintProfile("npxoffline", opts({ npxOffline: false, npxFetch: trackingFetch }));
|
|
304
|
+
expect(fetched).toBe(true);
|
|
305
|
+
});
|
|
240
306
|
});
|
|
241
307
|
|
|
242
308
|
describe("lintAllProfiles", () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The rules below are intentionally numbered and centralized so a later
|
|
5
5
|
* suppression pass can key off stable ids such as `# lint: ignore W1`.
|
|
6
6
|
*
|
|
7
|
-
* W1: profile declares more than
|
|
7
|
+
* W1: profile declares more than 120 skills.
|
|
8
8
|
* W2: profile declares more than 5 MCP servers.
|
|
9
9
|
* W3: inheritance chain depth is greater than 2.
|
|
10
10
|
* W4: a skill slug appears in both `skills.local` and `skills.npx`.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { mkdir, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
21
|
-
import type
|
|
21
|
+
import { existsSync, type Dirent } from "node:fs";
|
|
22
22
|
import { tmpdir } from "node:os";
|
|
23
23
|
import { dirname, join, resolve } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
SchemaViolation,
|
|
37
37
|
} from "../../profiles/_types";
|
|
38
38
|
import { listProfiles, loadProfile } from "./profile-loader";
|
|
39
|
-
import { materializeMcp, type MaterializeOptions } from "./mcp-materializer";
|
|
39
|
+
import { materializeMcp, McpNotFound, type MaterializeOptions } from "./mcp-materializer";
|
|
40
40
|
import { resolveLocal } from "./resolver-local";
|
|
41
41
|
import {
|
|
42
42
|
NpxFetchFailed,
|
|
@@ -57,7 +57,7 @@ const DEFAULT_HOOKS_ROOT = join(REPO_ROOT, "resources", "hooks");
|
|
|
57
57
|
const DEFAULT_SUBAGENTS_ROOT = join(REPO_ROOT, "resources", "subagents");
|
|
58
58
|
|
|
59
59
|
export type LintRuleId =
|
|
60
|
-
| "W1" | "W2" | "W3" | "W4" | "W5" | "W6" | "W7" | "W8"
|
|
60
|
+
| "W1" | "W2" | "W3" | "W4" | "W5" | "W6" | "W7" | "W8" | "W9"
|
|
61
61
|
| "E1" | "E2" | "E3";
|
|
62
62
|
export type DiagnosticRuleId = LintRuleId | "SCHEMA" | "LOAD";
|
|
63
63
|
export type LintSeverity = "warning" | "error";
|
|
@@ -72,7 +72,7 @@ export const PROFILE_LINT_RULES: Record<LintRuleId, RuleDoc> = {
|
|
|
72
72
|
W1: {
|
|
73
73
|
severity: "warning",
|
|
74
74
|
title: "too many skills",
|
|
75
|
-
description: "Profile declares more than
|
|
75
|
+
description: "Profile declares more than 120 skills; this can bloat prompt tokens.",
|
|
76
76
|
},
|
|
77
77
|
W2: {
|
|
78
78
|
severity: "warning",
|
|
@@ -109,6 +109,11 @@ export const PROFILE_LINT_RULES: Record<LintRuleId, RuleDoc> = {
|
|
|
109
109
|
title: "skill missing when_to_invoke",
|
|
110
110
|
description: "Skill has a capability but no explicit `when_to_invoke:` frontmatter — proactive routing falls back to a single generic row. Add `when_to_invoke:` with task-shape bullets for richer routing.",
|
|
111
111
|
},
|
|
112
|
+
W9: {
|
|
113
|
+
severity: "warning",
|
|
114
|
+
title: "local-only MCP",
|
|
115
|
+
description: "Profile references an MCP that has a source dir but isn't in the sanitized public registry — a private/local server the user wires into their own ~/.claude.json. Not a profile bug, so it's a warning, not E3.",
|
|
116
|
+
},
|
|
112
117
|
E1: {
|
|
113
118
|
severity: "error",
|
|
114
119
|
title: "profile name collision",
|
|
@@ -498,7 +503,7 @@ function checkStaticRules(
|
|
|
498
503
|
result: ProfileLintResult,
|
|
499
504
|
): void {
|
|
500
505
|
const skillCount = declaredSkillCount(profile);
|
|
501
|
-
if (skillCount >
|
|
506
|
+
if (skillCount > 120) {
|
|
502
507
|
addIssue(
|
|
503
508
|
result,
|
|
504
509
|
"W1",
|
|
@@ -614,19 +619,27 @@ async function checkNpxSkills(
|
|
|
614
619
|
}
|
|
615
620
|
|
|
616
621
|
let resolvedCount = 0;
|
|
622
|
+
let skippedOffline = 0;
|
|
617
623
|
for (const entry of entries) {
|
|
618
624
|
for (const skill of entry.skills) {
|
|
619
625
|
try {
|
|
620
|
-
await resolveOneNpxSkill(entry, skill, opts);
|
|
621
|
-
|
|
626
|
+
const status = await resolveOneNpxSkill(entry, skill, opts);
|
|
627
|
+
if (status === "skipped-offline") skippedOffline += 1;
|
|
628
|
+
else resolvedCount += 1;
|
|
622
629
|
} catch (err) {
|
|
623
630
|
addResolverIssue(result, "npx skill", `${entry.repo}:${skill}`, err);
|
|
624
631
|
}
|
|
625
632
|
}
|
|
626
633
|
}
|
|
627
634
|
|
|
628
|
-
if (resolvedCount === total) {
|
|
629
|
-
addCheck(
|
|
635
|
+
if (resolvedCount + skippedOffline === total) {
|
|
636
|
+
addCheck(
|
|
637
|
+
result,
|
|
638
|
+
"npx skills",
|
|
639
|
+
skippedOffline > 0
|
|
640
|
+
? `${resolvedCount} resolved, ${skippedOffline} not cached (offline; run --online to fetch)`
|
|
641
|
+
: `${resolvedCount} resolved or fetchable`,
|
|
642
|
+
);
|
|
630
643
|
}
|
|
631
644
|
}
|
|
632
645
|
|
|
@@ -634,7 +647,7 @@ async function resolveOneNpxSkill(
|
|
|
634
647
|
entry: NpxSkillRef,
|
|
635
648
|
skill: string,
|
|
636
649
|
opts: ProfileLinterOptions,
|
|
637
|
-
): Promise<
|
|
650
|
+
): Promise<"resolved" | "skipped-offline"> {
|
|
638
651
|
const single: Profile = {
|
|
639
652
|
name: "lint-npx",
|
|
640
653
|
description: "single npx resolver check",
|
|
@@ -649,9 +662,13 @@ async function resolveOneNpxSkill(
|
|
|
649
662
|
fetch: opts.npxFetch,
|
|
650
663
|
offline: true,
|
|
651
664
|
});
|
|
652
|
-
return;
|
|
665
|
+
return "resolved";
|
|
653
666
|
} catch (err) {
|
|
654
|
-
|
|
667
|
+
const offline = opts.npxOffline || (process.env.CUE_OFFLINE ?? process.env.SOUL_OFFLINE) === "1";
|
|
668
|
+
if (offline) {
|
|
669
|
+
// Offline mode: a cache miss is a benign "not cached", not a lint error.
|
|
670
|
+
// Real problems (PinNotFound, schema, etc.) still propagate to E3.
|
|
671
|
+
if (err instanceof NpxFetchFailed) return "skipped-offline";
|
|
655
672
|
throw err;
|
|
656
673
|
}
|
|
657
674
|
if (!(err instanceof NpxFetchFailed || err instanceof ProfileError)) {
|
|
@@ -670,6 +687,7 @@ async function resolveOneNpxSkill(
|
|
|
670
687
|
} finally {
|
|
671
688
|
await rm(tempRepo, { recursive: true, force: true });
|
|
672
689
|
}
|
|
690
|
+
return "resolved";
|
|
673
691
|
}
|
|
674
692
|
|
|
675
693
|
async function checkPlugins(
|
|
@@ -719,7 +737,22 @@ async function checkMcps(
|
|
|
719
737
|
});
|
|
720
738
|
resolvedCount += 1;
|
|
721
739
|
} catch (err) {
|
|
722
|
-
|
|
740
|
+
// A ref to an MCP that has a source dir but is absent from the sanitized
|
|
741
|
+
// public registry is a private/local-only server (configured in the
|
|
742
|
+
// user's own ~/.claude.json), not a profile bug — demote to W9, the same
|
|
743
|
+
// way PluginNotInstalled demotes to W5. A ref with no source dir is a
|
|
744
|
+
// genuine dangling reference and still fails with E3.
|
|
745
|
+
if (err instanceof McpNotFound && mcpHasLocalSource(ref.id, opts)) {
|
|
746
|
+
addIssue(
|
|
747
|
+
result,
|
|
748
|
+
"W9",
|
|
749
|
+
"warning",
|
|
750
|
+
`MCP "${ref.id}" is local-only — it has a source dir but isn't in the sanitized public registry; configure it in ~/.claude.json`,
|
|
751
|
+
{ subject: ref.id },
|
|
752
|
+
);
|
|
753
|
+
} else {
|
|
754
|
+
addResolverIssue(result, "MCP", ref.id, err);
|
|
755
|
+
}
|
|
723
756
|
}
|
|
724
757
|
}
|
|
725
758
|
|
|
@@ -778,6 +811,18 @@ function profileWithMcp(profile: ResolvedProfile, ref: ResolvedProfile["mcps"][n
|
|
|
778
811
|
};
|
|
779
812
|
}
|
|
780
813
|
|
|
814
|
+
/**
|
|
815
|
+
* True when an MCP id has a source manifest dir under `resources/mcps/mcps/<id>`
|
|
816
|
+
* even though it's absent from the sanitized public registry — i.e. a private /
|
|
817
|
+
* local-only server, not a dangling reference. Keyed off the same registry root
|
|
818
|
+
* the resolver uses (configsRoot's parent) so test overrides stay consistent.
|
|
819
|
+
*/
|
|
820
|
+
function mcpHasLocalSource(id: string, opts: ProfileLinterOptions): boolean {
|
|
821
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) return false;
|
|
822
|
+
const configsRoot = opts.configsRoot ?? DEFAULT_CONFIGS_ROOT;
|
|
823
|
+
return existsSync(join(dirname(configsRoot), "mcps", id));
|
|
824
|
+
}
|
|
825
|
+
|
|
781
826
|
function formatErrorMessage(err: unknown): string {
|
|
782
827
|
if (err instanceof ProfileError) {
|
|
783
828
|
return `${err.code}: ${err.message}`;
|
|
@@ -32,10 +32,15 @@ const EXAMPLES_DIR = join(REPO_ROOT, "profiles", "_examples");
|
|
|
32
32
|
|
|
33
33
|
let scratchRoot: string;
|
|
34
34
|
let priorEnv: string | undefined;
|
|
35
|
+
// SOUL_PROFILES_DIR is a fallback the loader honors; one test below sets it.
|
|
36
|
+
// Capture + restore it too so it never leaks into sibling test files (a dead
|
|
37
|
+
// SOUL_PROFILES_DIR would make their loadProfile() calls fail).
|
|
38
|
+
let priorSoulEnv: string | undefined;
|
|
35
39
|
|
|
36
40
|
beforeEach(async () => {
|
|
37
41
|
scratchRoot = await mkdtemp(join(tmpdir(), "cue-profile-loader-"));
|
|
38
42
|
priorEnv = process.env.CUE_PROFILES_DIR;
|
|
43
|
+
priorSoulEnv = process.env.SOUL_PROFILES_DIR;
|
|
39
44
|
process.env.CUE_PROFILES_DIR = scratchRoot;
|
|
40
45
|
});
|
|
41
46
|
|
|
@@ -45,6 +50,11 @@ afterEach(() => {
|
|
|
45
50
|
} else {
|
|
46
51
|
process.env.CUE_PROFILES_DIR = priorEnv;
|
|
47
52
|
}
|
|
53
|
+
if (priorSoulEnv === undefined) {
|
|
54
|
+
delete process.env.SOUL_PROFILES_DIR;
|
|
55
|
+
} else {
|
|
56
|
+
process.env.SOUL_PROFILES_DIR = priorSoulEnv;
|
|
57
|
+
}
|
|
48
58
|
});
|
|
49
59
|
|
|
50
60
|
/** Write a profile.yaml at `<scratchRoot>/<name>/profile.yaml`. */
|
|
@@ -413,6 +423,17 @@ describe("parseProfileSelector", () => {
|
|
|
413
423
|
expect(() => parseProfileSelector("+++")).toThrow(/empty/i);
|
|
414
424
|
});
|
|
415
425
|
|
|
426
|
+
test("duplicate parts collapse, first-occurrence order preserved", () => {
|
|
427
|
+
// A stale pin / Recent row can carry repeats — loading a profile twice
|
|
428
|
+
// bloats the materialized CLAUDE.md, so the parser de-dupes.
|
|
429
|
+
expect(parseProfileSelector("gstack+higgsfield+postizz+higgsfield+postizz+gstack")).toEqual([
|
|
430
|
+
"gstack",
|
|
431
|
+
"higgsfield",
|
|
432
|
+
"postizz",
|
|
433
|
+
]);
|
|
434
|
+
expect(parseProfileSelector("a+a+a")).toEqual(["a"]);
|
|
435
|
+
});
|
|
436
|
+
|
|
416
437
|
test("isCompositeSelector recognizes ≥2 parts", () => {
|
|
417
438
|
expect(isCompositeSelector("postizz")).toBe(false);
|
|
418
439
|
expect(isCompositeSelector("postizz+trendradar")).toBe(true);
|
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
type ResolvedPlugin,
|
|
34
34
|
type ResolvedProfile,
|
|
35
35
|
type ResolvedSkill,
|
|
36
|
-
type SkillCondition,
|
|
37
36
|
type SkillRef,
|
|
38
37
|
SchemaViolation,
|
|
39
38
|
} from "../../profiles/_types";
|
|
@@ -480,6 +479,7 @@ function foldChain(chain: Profile[]): ResolvedProfile {
|
|
|
480
479
|
qualityGates: dedupePrimitiveArray(acc.qualityGates, child.qualityGates),
|
|
481
480
|
evals: dedupePrimitiveArray(acc.evals, child.evals),
|
|
482
481
|
recommends: dedupePrimitiveArray(acc.recommends, child.recommends),
|
|
482
|
+
autoSelect: dedupePrimitiveArray(acc.autoSelect, child.autoSelect),
|
|
483
483
|
conflicts: dedupePrimitiveArray(acc.conflicts, child.conflicts),
|
|
484
484
|
// bundles is a display hint, leaf-wins: a child that declares its own
|
|
485
485
|
// list overrides the parent; a child that omits it inherits the parent's.
|
|
@@ -523,6 +523,7 @@ function normalizeToResolved(p: Profile, chain: string[]): ResolvedProfile {
|
|
|
523
523
|
qualityGates: [...(p.qualityGates ?? [])],
|
|
524
524
|
evals: [...(p.evals ?? [])],
|
|
525
525
|
recommends: [...(p.recommends ?? [])],
|
|
526
|
+
autoSelect: [...(p.autoSelect ?? [])],
|
|
526
527
|
conflicts: [...(p.conflicts ?? [])],
|
|
527
528
|
bundles: p.bundles && p.bundles.length > 0 ? [...p.bundles] : undefined,
|
|
528
529
|
personaRouting: [...(p.persona_routing ?? [])],
|
|
@@ -549,7 +550,19 @@ export function parseProfileSelector(selector: string): string[] {
|
|
|
549
550
|
`Profile selector "${selector}" is empty after parsing`,
|
|
550
551
|
);
|
|
551
552
|
}
|
|
552
|
-
|
|
553
|
+
// Collapse duplicate parts, order preserved. A selector can accumulate
|
|
554
|
+
// repeats — a stale pin, a Recent/Featured row built from an already-duped
|
|
555
|
+
// selector, or a composite primary whose part is also offered as a companion.
|
|
556
|
+
// Loading the same profile twice bloats the materialized CLAUDE.md and the
|
|
557
|
+
// token count for no benefit (skills/mcps fold to a set anyway).
|
|
558
|
+
const seen = new Set<string>();
|
|
559
|
+
const unique: string[] = [];
|
|
560
|
+
for (const part of parts) {
|
|
561
|
+
if (seen.has(part)) continue;
|
|
562
|
+
seen.add(part);
|
|
563
|
+
unique.push(part);
|
|
564
|
+
}
|
|
565
|
+
return unique;
|
|
553
566
|
}
|
|
554
567
|
|
|
555
568
|
/** True when the selector names two or more profiles to merge. */
|
|
@@ -603,6 +616,7 @@ function foldComposite(selector: string, parts: ResolvedProfile[]): ResolvedProf
|
|
|
603
616
|
qualityGates: [...head.qualityGates],
|
|
604
617
|
evals: [...head.evals],
|
|
605
618
|
recommends: [...head.recommends],
|
|
619
|
+
autoSelect: [...head.autoSelect],
|
|
606
620
|
conflicts: [...head.conflicts],
|
|
607
621
|
// persona_includes is additive across a composite too — policy snippets
|
|
608
622
|
// (Integrity Protocol, voice rules) from every stacked profile survive.
|
|
@@ -639,6 +653,7 @@ function foldComposite(selector: string, parts: ResolvedProfile[]): ResolvedProf
|
|
|
639
653
|
qualityGates: dedupePrimitiveArray(acc.qualityGates, next.qualityGates),
|
|
640
654
|
evals: dedupePrimitiveArray(acc.evals, next.evals),
|
|
641
655
|
recommends: dedupePrimitiveArray(acc.recommends, next.recommends),
|
|
656
|
+
autoSelect: dedupePrimitiveArray(acc.autoSelect, next.autoSelect),
|
|
642
657
|
conflicts: dedupePrimitiveArray(acc.conflicts, next.conflicts),
|
|
643
658
|
personaIncludes: dedupePrimitiveArray(acc.personaIncludes, next.personaIncludes),
|
|
644
659
|
personaRouting: [...acc.personaRouting, ...next.personaRouting],
|
|
@@ -676,7 +691,11 @@ export async function loadProfile(name: string): Promise<ResolvedProfile> {
|
|
|
676
691
|
const chain = await buildInheritanceChain(part);
|
|
677
692
|
resolved.push(foldChain(chain));
|
|
678
693
|
}
|
|
679
|
-
|
|
694
|
+
// Name the composite from the deduped parts, not the raw selector: a duped
|
|
695
|
+
// selector ("a+b+a") loads correct (once-each) resources but must not name
|
|
696
|
+
// a redundant runtime dir / CLAUDE.md path — that splits the materializer
|
|
697
|
+
// cache and resurrects the duplicated-profile display bug downstream.
|
|
698
|
+
return foldComposite(parts.join("+"), resolved);
|
|
680
699
|
}
|
|
681
700
|
|
|
682
701
|
/**
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* `core+skill-writer` sessions count toward the same skill.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { readFileSync, existsSync,
|
|
24
|
-
import { homedir } from "node:os";
|
|
23
|
+
import { readFileSync, existsSync, } from "node:fs";
|
|
25
24
|
import { join, resolve, dirname } from "node:path";
|
|
26
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { configDir } from "./config-paths";
|
|
27
27
|
|
|
28
28
|
const REPO_ROOT = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
29
29
|
export const SKILLS_ROOT = join(REPO_ROOT, "resources", "skills", "skills");
|
|
@@ -32,10 +32,6 @@ export function estimateTokens(text: string): number {
|
|
|
32
32
|
return Math.ceil(text.length / 4);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function configDir(): string {
|
|
36
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
37
|
-
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"), "cue");
|
|
38
|
-
}
|
|
39
35
|
|
|
40
36
|
/** Split a SKILL.md into its YAML frontmatter and body. */
|
|
41
37
|
function splitFrontmatter(text: string): { frontmatter: string; body: string } {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard: every profile name referenced by the auto-detect signals and the
|
|
3
|
+
* discover/ai keyword maps must correspond to a real profile directory under
|
|
4
|
+
* profiles/.
|
|
5
|
+
*
|
|
6
|
+
* Phantom names used to slip in and silently no-op: `launch.ts` filters
|
|
7
|
+
* detections down to known profile names (so a phantom suggestion is dropped
|
|
8
|
+
* with no feedback), and `cue discover install` skips writing skills into a
|
|
9
|
+
* profile.yaml that doesn't exist. This test fails fast if any suggestion ever
|
|
10
|
+
* points at a profile that isn't on disk — it caught `python-api`, `rust-cli`,
|
|
11
|
+
* `ecc`, and a bare `medusa` when it was written.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { SIGNALS } from "./auto-detect";
|
|
18
|
+
import { STACK_PROFILES, PROFILE_KEYWORDS as DISCOVER_KEYWORDS } from "../commands/discover";
|
|
19
|
+
import { PROFILE_KEYWORDS as AI_KEYWORDS } from "../commands/ai";
|
|
20
|
+
|
|
21
|
+
const PROFILES_DIR = resolve(import.meta.dir, "../../profiles");
|
|
22
|
+
|
|
23
|
+
function realProfiles(): Set<string> {
|
|
24
|
+
return new Set(
|
|
25
|
+
readdirSync(PROFILES_DIR, { withFileTypes: true })
|
|
26
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("_"))
|
|
27
|
+
.map((e) => e.name)
|
|
28
|
+
.filter((name) => existsSync(join(PROFILES_DIR, name, "profile.yaml"))),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("profile-name references resolve to real profiles", () => {
|
|
33
|
+
const real = realProfiles();
|
|
34
|
+
|
|
35
|
+
test("profiles/ fixture is populated", () => {
|
|
36
|
+
expect(real.size).toBeGreaterThan(10);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("auto-detect SIGNALS reference only real profiles", () => {
|
|
40
|
+
const missing = [...new Set(SIGNALS.map((s) => s.profile))].filter((p) => !real.has(p));
|
|
41
|
+
expect(missing).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("discover STACK_PROFILES reference only real profiles", () => {
|
|
45
|
+
const missing = [...STACK_PROFILES].filter((p) => !real.has(p));
|
|
46
|
+
expect(missing).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("discover PROFILE_KEYWORDS keys are real profiles", () => {
|
|
50
|
+
const missing = Object.keys(DISCOVER_KEYWORDS).filter((p) => !real.has(p));
|
|
51
|
+
expect(missing).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("ai PROFILE_KEYWORDS keys are real profiles", () => {
|
|
55
|
+
const missing = Object.keys(AI_KEYWORDS).filter((p) => !real.has(p));
|
|
56
|
+
expect(missing).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the repo provenance matcher — the pure logic behind the studio
|
|
3
|
+
* Profiles "Repos" tab. No network: only `reposForProfile` (the catalog filter)
|
|
4
|
+
* is exercised; the live star fetch is integration-tested via the live endpoint.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import { reposForProfile, REPO_CATALOG } from "./repos";
|
|
10
|
+
|
|
11
|
+
const slugs = (rs: { repo: string }[]) => rs.map((r) => r.repo).sort();
|
|
12
|
+
|
|
13
|
+
describe("reposForProfile", () => {
|
|
14
|
+
test("matches a repo by a namespace the profile has", () => {
|
|
15
|
+
const got = reposForProfile({ namespaces: ["meta"], mcpIds: [], pluginIds: [] });
|
|
16
|
+
expect(slugs(got)).toContain("opencue/cuecards");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("matches a repo by an MCP id the profile connects", () => {
|
|
20
|
+
const got = reposForProfile({ namespaces: [], mcpIds: ["lightpanda"], pluginIds: [] });
|
|
21
|
+
expect(slugs(got)).toEqual(["lightpanda-io/browser"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("matches a plugin by its bare name, ignoring the @marketplace suffix", () => {
|
|
25
|
+
const got = reposForProfile({ namespaces: [], mcpIds: [], pluginIds: ["claude-mem@thedotmack"] });
|
|
26
|
+
expect(slugs(got)).toEqual(["thedotmack/claude-mem"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("npx skills map to the anthropics/skills bundle", () => {
|
|
30
|
+
const got = reposForProfile({ namespaces: ["npx"], mcpIds: [], pluginIds: [] });
|
|
31
|
+
expect(slugs(got)).toContain("anthropics/skills");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("profile-specific MCPs/plugins map to their upstream repos", () => {
|
|
35
|
+
// The coolify MCP → the real Coolify repo (the user's pointed-at case).
|
|
36
|
+
expect(slugs(reposForProfile({ namespaces: [], mcpIds: ["coolify"], pluginIds: [] }))).toEqual(["coollabsio/coolify"]);
|
|
37
|
+
expect(slugs(reposForProfile({ namespaces: [], mcpIds: ["supabase"], pluginIds: [] }))).toEqual(["supabase/supabase"]);
|
|
38
|
+
expect(slugs(reposForProfile({ namespaces: [], mcpIds: [], pluginIds: ["vercel@claude-plugins-official"] }))).toEqual(["vercel/vercel"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns nothing for a profile that contains none of the catalog's sources", () => {
|
|
42
|
+
const got = reposForProfile({ namespaces: ["nonexistent-ns"], mcpIds: ["nope"], pluginIds: ["nope@x"] });
|
|
43
|
+
expect(got).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("dedupes — a repo matched by both ns and mcp appears once", () => {
|
|
47
|
+
const got = reposForProfile({ namespaces: ["browser"], mcpIds: ["lightpanda"], pluginIds: [] });
|
|
48
|
+
expect(got.filter((r) => r.repo === "lightpanda-io/browser")).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("every catalog repo is a real owner/name slug (no spaces, exactly one slash)", () => {
|
|
52
|
+
for (const r of REPO_CATALOG) {
|
|
53
|
+
expect(r.repo).toMatch(/^[^/\s]+\/[^/\s]+$/);
|
|
54
|
+
expect(r.kinds.length).toBeGreaterThan(0);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|