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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdtemp, readFile, rm, mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, readFile, writeFile, rm, mkdir, stat, chmod } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
import { runInstall, runUninstall } from "./shell";
|
|
6
|
+
import { runInstall, runUninstall, shimInstalled, resolveCueInvocation } from "./shell";
|
|
7
7
|
|
|
8
8
|
let fakeHome: string;
|
|
9
9
|
beforeEach(async () => {
|
|
@@ -22,10 +22,13 @@ describe("shell install", () => {
|
|
|
22
22
|
});
|
|
23
23
|
expect(rc).toBe(0);
|
|
24
24
|
|
|
25
|
+
// Assert on `launch <agent>` (present in both the bare and absolute-path
|
|
26
|
+
// shim forms) rather than the exact invocation token, which depends on
|
|
27
|
+
// whether `cue` is resolvable on the test runner's PATH.
|
|
25
28
|
const claudeShim = await readFile(join(fakeHome, ".local", "bin", "claude"), "utf8");
|
|
26
|
-
expect(claudeShim).toContain("
|
|
29
|
+
expect(claudeShim).toContain("launch claude");
|
|
27
30
|
const codexShim = await readFile(join(fakeHome, ".local", "bin", "codex"), "utf8");
|
|
28
|
-
expect(codexShim).toContain("
|
|
31
|
+
expect(codexShim).toContain("launch codex");
|
|
29
32
|
|
|
30
33
|
const st = await stat(join(fakeHome, ".local", "bin", "claude"));
|
|
31
34
|
expect((st.mode & 0o111) !== 0).toBe(true); // executable
|
|
@@ -53,3 +56,61 @@ describe("shell install", () => {
|
|
|
53
56
|
await expect(stat(join(fakeHome, ".local", "bin", "claude"))).rejects.toThrow();
|
|
54
57
|
});
|
|
55
58
|
});
|
|
59
|
+
|
|
60
|
+
describe("shimInstalled", () => {
|
|
61
|
+
const shimPath = () => join(fakeHome, ".local", "bin", "claude");
|
|
62
|
+
|
|
63
|
+
test("false when no shim exists", () => {
|
|
64
|
+
expect(shimInstalled(fakeHome)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("true for the runInstall() bare format (exec cue launch claude)", async () => {
|
|
68
|
+
await writeFile(shimPath(), '#!/usr/bin/env bash\nexec cue launch claude "$@"\n');
|
|
69
|
+
expect(shimInstalled(fakeHome)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("true for the `cue shell install` absolute-path format", async () => {
|
|
73
|
+
// This is the exact format run(["install"]) writes — the case the original
|
|
74
|
+
// `cue launch` substring check false-negatived on.
|
|
75
|
+
await writeFile(shimPath(), '#!/usr/bin/env bash\nexec "/home/u/Documents/cue/bin/cue" launch claude "$@"\n');
|
|
76
|
+
expect(shimInstalled(fakeHome)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("false for a non-cue claude on PATH", async () => {
|
|
80
|
+
await writeFile(shimPath(), '#!/usr/bin/env bash\nexec /opt/anthropic/claude "$@"\n');
|
|
81
|
+
expect(shimInstalled(fakeHome)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("resolveCueInvocation", () => {
|
|
86
|
+
test("returns bare `cue` when an executable cue is resolvable on PATH (npm-global case)", async () => {
|
|
87
|
+
const binDir = join(fakeHome, "pathbin");
|
|
88
|
+
await mkdir(binDir, { recursive: true });
|
|
89
|
+
await writeFile(join(binDir, "cue"), "#!/bin/sh\n");
|
|
90
|
+
await chmod(join(binDir, "cue"), 0o755);
|
|
91
|
+
expect(resolveCueInvocation({ pathDirs: [binDir] })).toBe("cue");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("ignores a non-executable cue on PATH and falls back to the abspath", async () => {
|
|
95
|
+
const binDir = join(fakeHome, "pathbin2");
|
|
96
|
+
await mkdir(binDir, { recursive: true });
|
|
97
|
+
await writeFile(join(binDir, "cue"), "not executable\n"); // no chmod +x
|
|
98
|
+
const repoRoot = join(fakeHome, "repo2");
|
|
99
|
+
await mkdir(join(repoRoot, "bin"), { recursive: true });
|
|
100
|
+
await writeFile(join(repoRoot, "bin", "cue"), "#!/usr/bin/env bun\n");
|
|
101
|
+
const out = resolveCueInvocation({ pathDirs: [binDir], repoRoot });
|
|
102
|
+
expect(out).toBe(`"${join(repoRoot, "bin", "cue")}"`);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("falls back to a quoted absolute path when cue is not on PATH (source clone)", async () => {
|
|
106
|
+
const repoRoot = join(fakeHome, "repo");
|
|
107
|
+
await mkdir(join(repoRoot, "bin"), { recursive: true });
|
|
108
|
+
await writeFile(join(repoRoot, "bin", "cue"), "#!/usr/bin/env bun\n");
|
|
109
|
+
const emptyDir = join(fakeHome, "empty");
|
|
110
|
+
await mkdir(emptyDir, { recursive: true });
|
|
111
|
+
const out = resolveCueInvocation({ pathDirs: [emptyDir], repoRoot });
|
|
112
|
+
expect(out).toBe(`"${join(repoRoot, "bin", "cue")}"`);
|
|
113
|
+
// Either form must keep the `launch claude` substring intact downstream.
|
|
114
|
+
expect(`exec ${out} launch claude "$@"`).toContain("launch claude");
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/commands/shell.ts
CHANGED
|
@@ -6,10 +6,22 @@
|
|
|
6
6
|
* Adds a cd wrapper that checks .cue-profile on directory change.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
9
|
+
import { existsSync, readFileSync, statSync, accessSync, constants } from "node:fs";
|
|
10
10
|
import { join, resolve } from "node:path";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
|
|
13
|
+
/** True when `p` is an executable regular file (mirrors how the shell resolves
|
|
14
|
+
* a command on PATH — skips directories and non-executable files). */
|
|
15
|
+
function isExecutableFile(p: string): boolean {
|
|
16
|
+
try {
|
|
17
|
+
if (!statSync(p).isFile()) return false;
|
|
18
|
+
accessSync(p, constants.X_OK);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
function hookBash(): string {
|
|
14
26
|
return `# cue shell hook — auto-switch profile on cd
|
|
15
27
|
__cue_cd() {
|
|
@@ -92,6 +104,52 @@ export interface ShimOptions {
|
|
|
92
104
|
realCodex?: string;
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
/**
|
|
108
|
+
* True when the `claude` shim is already installed in ~/.local/bin and is a
|
|
109
|
+
* cue launch shim. Matches both shim formats cue writes: the user-facing
|
|
110
|
+
* `cue shell install` form (`exec "<abs-path>/cue" launch claude "$@"`) and the
|
|
111
|
+
* runInstall() helper form (`exec cue launch claude "$@"`) — both contain the
|
|
112
|
+
* literal `launch claude`. Used by `cue init` to detect that profile loading
|
|
113
|
+
* hasn't been activated yet. Conservative: any read error → false.
|
|
114
|
+
*/
|
|
115
|
+
export function shimInstalled(homeDir?: string): boolean {
|
|
116
|
+
const shimPath = join(homeDir ?? homedir(), ".local", "bin", "claude");
|
|
117
|
+
try {
|
|
118
|
+
return existsSync(shimPath) && readFileSync(shimPath, "utf8").includes("launch claude");
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the token to place after `exec ` in a shim so it works for BOTH
|
|
126
|
+
* npm-global installs (cue is on PATH, there is no source clone) and
|
|
127
|
+
* source-clone users.
|
|
128
|
+
*
|
|
129
|
+
* - Prefers the portable bare `cue` when it's resolvable on PATH (the
|
|
130
|
+
* npm-global-correct form, and the form the docs advertise).
|
|
131
|
+
* - Otherwise falls back to an absolute path to the cue entrypoint
|
|
132
|
+
* (CUE_REPO_ROOT is exported by both `bin/cue` and `bin/cue.mjs` when cue
|
|
133
|
+
* runs itself), double-quoted for the shell.
|
|
134
|
+
*
|
|
135
|
+
* Either form keeps the literal `launch <agent>` substring the caller
|
|
136
|
+
* appends, so shimInstalled() detects both. Previously `cue shell install`
|
|
137
|
+
* hard-coded `~/Documents/cue/bin/cue`, which doesn't exist for npm-global
|
|
138
|
+
* users — the shim pointed at a missing file and `claude` broke.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveCueInvocation(opts: { repoRoot?: string; pathDirs?: string[] } = {}): string {
|
|
141
|
+
const pathDirs = opts.pathDirs ?? (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
142
|
+
for (const dir of pathDirs) {
|
|
143
|
+
if (dir && isExecutableFile(join(dir, "cue"))) return "cue";
|
|
144
|
+
}
|
|
145
|
+
// Prefer the node entrypoint (npm layout) then the bash one, then ~/Documents.
|
|
146
|
+
const root = opts.repoRoot ?? process.env.CUE_REPO_ROOT ?? join(homedir(), "Documents", "cue");
|
|
147
|
+
for (const candidate of [join(root, "bin", "cue.mjs"), join(root, "bin", "cue")]) {
|
|
148
|
+
if (existsSync(candidate)) return `"${candidate}"`;
|
|
149
|
+
}
|
|
150
|
+
return `"${process.argv[1] ?? "cue"}"`;
|
|
151
|
+
}
|
|
152
|
+
|
|
95
153
|
export async function runInstall(opts: ShimOptions = {}): Promise<number> {
|
|
96
154
|
const home = opts.homeDir ?? homedir();
|
|
97
155
|
const shimDir = join(home, ".local", "bin");
|
|
@@ -111,14 +169,14 @@ export async function runInstall(opts: ShimOptions = {}): Promise<number> {
|
|
|
111
169
|
}
|
|
112
170
|
|
|
113
171
|
mkdirSync(shimDir, { recursive: true });
|
|
114
|
-
const
|
|
172
|
+
const cueInvoke = resolveCueInvocation();
|
|
115
173
|
|
|
116
|
-
const claudeShim = `#!/usr/bin/env bash\nexec
|
|
174
|
+
const claudeShim = `#!/usr/bin/env bash\nexec ${cueInvoke} launch claude "$@"\n`;
|
|
117
175
|
writeFileSync(join(shimDir, "claude"), claudeShim);
|
|
118
176
|
chmodSync(join(shimDir, "claude"), 0o755);
|
|
119
177
|
|
|
120
178
|
if (opts.realCodex) {
|
|
121
|
-
const codexShim = `#!/usr/bin/env bash\nexec
|
|
179
|
+
const codexShim = `#!/usr/bin/env bash\nexec ${cueInvoke} launch codex "$@"\n`;
|
|
122
180
|
writeFileSync(join(shimDir, "codex"), codexShim);
|
|
123
181
|
chmodSync(join(shimDir, "codex"), 0o755);
|
|
124
182
|
}
|
|
@@ -161,11 +219,13 @@ export async function run(args: string[]): Promise<number> {
|
|
|
161
219
|
const { mkdirSync, writeFileSync, chmodSync } = await import("node:fs");
|
|
162
220
|
mkdirSync(shimDir, { recursive: true });
|
|
163
221
|
|
|
164
|
-
|
|
222
|
+
// Portable invocation token: bare `cue` when on PATH (npm-global), else
|
|
223
|
+
// an absolute path to the cue entrypoint. See resolveCueInvocation.
|
|
224
|
+
const cueInvoke = resolveCueInvocation();
|
|
165
225
|
|
|
166
226
|
// Claude shim
|
|
167
227
|
const claudeShim = `#!/usr/bin/env bash
|
|
168
|
-
exec
|
|
228
|
+
exec ${cueInvoke} launch claude "$@"
|
|
169
229
|
`;
|
|
170
230
|
writeFileSync(join(shimDir, "claude"), claudeShim);
|
|
171
231
|
chmodSync(join(shimDir, "claude"), 0o755);
|
|
@@ -174,7 +234,7 @@ exec "${cueBin}" launch claude "$@"
|
|
|
174
234
|
// Codex shim (optional)
|
|
175
235
|
if (args.includes("--codex")) {
|
|
176
236
|
const codexShim = `#!/usr/bin/env bash
|
|
177
|
-
exec
|
|
237
|
+
exec ${cueInvoke} launch codex "$@"
|
|
178
238
|
`;
|
|
179
239
|
writeFileSync(join(shimDir, "codex"), codexShim);
|
|
180
240
|
chmodSync(join(shimDir, "codex"), 0o755);
|
|
@@ -65,7 +65,6 @@ function loadTestCases(skillId: string): TestCase[] {
|
|
|
65
65
|
function runTest(skillId: string, testCase: TestCase): TestResult {
|
|
66
66
|
const skillPath = join(SKILLS_ROOT, skillId, "SKILL.md");
|
|
67
67
|
const skillContent = existsSync(skillPath) ? readFileSync(skillPath, "utf8").toLowerCase() : "";
|
|
68
|
-
const input = testCase.input.toLowerCase();
|
|
69
68
|
const failures: string[] = [];
|
|
70
69
|
|
|
71
70
|
// Check: does the skill description match the input context?
|
package/src/commands/skills.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { listProfiles, loadProfile } from "../lib/profile-loader";
|
|
|
23
23
|
import { resolveActiveProfile } from "../lib/cwd-resolver";
|
|
24
24
|
import { listAllSkillIds } from "../lib/resolver-local";
|
|
25
25
|
import { fetchCompanionFiles, readSourceFile, findIncompleteSkills } from "../lib/companion-fetch";
|
|
26
|
+
import { gateFreshSkill } from "./security";
|
|
26
27
|
|
|
27
28
|
const REPO_ROOT = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
28
29
|
const PROFILES_DIR = process.env.CUE_PROFILES_DIR ?? join(REPO_ROOT, "profiles");
|
|
@@ -442,7 +443,9 @@ async function cmdNpxAdd(args: string[]): Promise<number> {
|
|
|
442
443
|
const repo = addArgs.find((a) => !a.startsWith("-") && !["claude-code", "codex", "*"].includes(a));
|
|
443
444
|
|
|
444
445
|
// #6: If -y/--yes without --skill, inject --skill "*" to skip npx's interactive picker
|
|
445
|
-
|
|
446
|
+
// (strip our own --allow-unsafe so it isn't forwarded to `npx skills add`).
|
|
447
|
+
const allowUnsafe = args.includes("--allow-unsafe");
|
|
448
|
+
const passedArgs = args.filter((a) => a !== "--allow-unsafe");
|
|
446
449
|
const hasYes = passedArgs.includes("-y") || passedArgs.includes("--yes");
|
|
447
450
|
const hasSkill = passedArgs.includes("-s") || passedArgs.includes("--skill");
|
|
448
451
|
if (hasYes && !hasSkill) {
|
|
@@ -491,6 +494,30 @@ async function cmdNpxAdd(args: string[]): Promise<number> {
|
|
|
491
494
|
// If still nothing, the install was likely cancelled — don't trigger hook
|
|
492
495
|
if (newSkills.length === 0) return 0;
|
|
493
496
|
|
|
497
|
+
// Security gate: scan freshly-fetched skills before the profile hook below
|
|
498
|
+
// registers any into profile.yaml. Block criticals (SEC1-3) unless
|
|
499
|
+
// --allow-unsafe; flagged skills stay on disk but are dropped from the set.
|
|
500
|
+
{
|
|
501
|
+
const blocked: string[] = [];
|
|
502
|
+
for (const slug of newSkills) {
|
|
503
|
+
const gate = gateFreshSkill(slug, { allowUnsafe });
|
|
504
|
+
if (!gate.ok) {
|
|
505
|
+
blocked.push(slug);
|
|
506
|
+
process.stderr.write(`🔴 BLOCKED ${slug}: ${gate.critical.length} critical security finding(s)\n`);
|
|
507
|
+
for (const c of gate.critical) {
|
|
508
|
+
process.stderr.write(` [${c.code}] ${c.message}${c.line ? ` (line ${c.line})` : ""}\n`);
|
|
509
|
+
}
|
|
510
|
+
} else if (!gate.scanned) {
|
|
511
|
+
process.stderr.write(`⚠️ ${slug}: no SKILL.md found to scan — review manually.\n`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (blocked.length > 0) {
|
|
515
|
+
newSkills = newSkills.filter((s) => !blocked.includes(s));
|
|
516
|
+
process.stderr.write(` ${blocked.length} skill(s) left on disk but NOT registered. Re-run with --allow-unsafe to register anyway.\n`);
|
|
517
|
+
if (newSkills.length === 0) return 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
494
521
|
// #7: Fetch GitHub repo description
|
|
495
522
|
let repoDescription = "";
|
|
496
523
|
if (repo) {
|
|
@@ -1525,7 +1552,6 @@ function cmdChangelog(id: string): number {
|
|
|
1525
1552
|
return 1;
|
|
1526
1553
|
}
|
|
1527
1554
|
|
|
1528
|
-
const meta = parseSkillMeta(id);
|
|
1529
1555
|
const skillPath = join(SKILLS_ROOT, id, "SKILL.md");
|
|
1530
1556
|
try {
|
|
1531
1557
|
const content = readFileSync(skillPath, "utf8");
|
package/src/commands/sources.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { loadProfile, listProfiles } from "../lib/profile-loader";
|
|
|
17
17
|
|
|
18
18
|
const REPO_ROOT = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
19
19
|
const SKILLS_LOCK = join(homedir(), "skills-lock.json");
|
|
20
|
-
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
21
20
|
|
|
22
21
|
interface LockEntry {
|
|
23
22
|
source: string;
|
|
@@ -97,7 +96,7 @@ function cmdList(json: boolean): number {
|
|
|
97
96
|
async function cmdProfile(profileName: string, json: boolean): Promise<number> {
|
|
98
97
|
const lock = loadSkillsLock();
|
|
99
98
|
let profile;
|
|
100
|
-
try { profile = await loadProfile(profileName); } catch
|
|
99
|
+
try { profile = await loadProfile(profileName); } catch {
|
|
101
100
|
process.stderr.write(`Profile "${profileName}" not found.\n`);
|
|
102
101
|
return 1;
|
|
103
102
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join, resolve, dirname } from "node:path";
|
|
9
|
-
import { existsSync, readFileSync, readdirSync,
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, } from "node:fs";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
|
|
12
|
+
import { configDir } from "../lib/config-paths";
|
|
12
13
|
import { resolveProfileForCwd } from "../lib/cwd-resolver";
|
|
13
14
|
import { loadProfile, listProfiles } from "../lib/profile-loader";
|
|
14
15
|
import { computeStats } from "../lib/analytics";
|
|
@@ -19,11 +20,6 @@ const SKILLS_ROOT = join(REPO_ROOT, "resources", "skills", "skills");
|
|
|
19
20
|
const MCP_CONFIGS_DIR = join(REPO_ROOT, "resources", "mcps", "configs");
|
|
20
21
|
const RUNTIME_ROOT = join(process.env.HOME ?? "~", ".config", "cue", "runtime");
|
|
21
22
|
|
|
22
|
-
function configDir(): string {
|
|
23
|
-
return process.env.XDG_CONFIG_HOME
|
|
24
|
-
? join(process.env.XDG_CONFIG_HOME, "cue")
|
|
25
|
-
: join(homedir(), ".config", "cue");
|
|
26
|
-
}
|
|
27
23
|
|
|
28
24
|
export interface Warning {
|
|
29
25
|
code: string;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { existsSync, mkdtempSync, mkdirSync, copyFileSync, readFileSync } from "node:fs";
|
|
15
15
|
import { tmpdir, homedir } from "node:os";
|
|
16
|
-
import { join,
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
17
|
import { spawnSync } from "node:child_process";
|
|
18
18
|
|
|
19
19
|
import { parse as parseYaml } from "yaml";
|
package/src/commands/suggest.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { join, resolve, dirname } from "node:path";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
|
|
14
|
-
import { loadProfile,
|
|
14
|
+
import { loadProfile, } from "../lib/profile-loader";
|
|
15
15
|
import { listAllSkillIds } from "../lib/resolver-local";
|
|
16
16
|
import { resolveProfileForCwd } from "../lib/cwd-resolver";
|
|
17
17
|
|
|
@@ -131,33 +131,58 @@ function tokenizeText(text: string): string[] {
|
|
|
131
131
|
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter(t => t.length > 2);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Bound the session scan so `cue suggest` stays fast regardless of how large
|
|
135
|
+
// the user's history is: read the most-recent transcripts first, up to a file
|
|
136
|
+
// and total-byte budget. Without this, a heavy user's ~/.claude/projects (many
|
|
137
|
+
// large .jsonl files) makes both this scan and scoreSkills run for minutes.
|
|
138
|
+
// `CUE_SUGGEST_SESSIONS_DIR` overrides the source dir (tests use it to stay
|
|
139
|
+
// hermetic). Transcripts beyond the budget are skipped — recent sessions are
|
|
140
|
+
// the relevant signal anyway.
|
|
141
|
+
const MAX_SESSION_FILES = 100;
|
|
142
|
+
const MAX_SESSION_BYTES = 2_000_000;
|
|
143
|
+
const PER_FILE_BYTES = 100_000;
|
|
144
|
+
|
|
134
145
|
function scanSessions(cutoffMs: number): string[] {
|
|
135
|
-
const projectsDir = join(homedir(), ".claude", "projects");
|
|
146
|
+
const projectsDir = process.env.CUE_SUGGEST_SESSIONS_DIR ?? join(homedir(), ".claude", "projects");
|
|
136
147
|
if (!existsSync(projectsDir)) return [];
|
|
137
148
|
|
|
138
|
-
|
|
149
|
+
// Collect candidate transcripts newer than the cutoff, with mtime for ranking.
|
|
150
|
+
const candidates: Array<{ path: string; mtimeMs: number }> = [];
|
|
139
151
|
try {
|
|
140
152
|
const dirs = readdirSync(projectsDir).filter(d => {
|
|
141
153
|
try { return statSync(join(projectsDir, d)).isDirectory(); } catch { return false; }
|
|
142
154
|
});
|
|
143
155
|
for (const dir of dirs) {
|
|
144
156
|
const dirPath = join(projectsDir, dir);
|
|
145
|
-
|
|
157
|
+
let files: string[];
|
|
158
|
+
try { files = readdirSync(dirPath).filter(f => f.endsWith(".jsonl")); } catch { continue; }
|
|
146
159
|
for (const f of files) {
|
|
147
160
|
const fPath = join(dirPath, f);
|
|
148
161
|
try {
|
|
149
162
|
const st = statSync(fPath);
|
|
150
163
|
if (st.mtimeMs < cutoffMs) continue;
|
|
151
|
-
|
|
152
|
-
const fd = require("node:fs").openSync(fPath, "r");
|
|
153
|
-
const buf = Buffer.alloc(100_000);
|
|
154
|
-
const n = require("node:fs").readSync(fd, buf, 0, 100_000, 0);
|
|
155
|
-
require("node:fs").closeSync(fd);
|
|
156
|
-
chunks.push(buf.toString("utf8", 0, n));
|
|
164
|
+
candidates.push({ path: fPath, mtimeMs: st.mtimeMs });
|
|
157
165
|
} catch {}
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
} catch {}
|
|
169
|
+
|
|
170
|
+
// Most-recent first, then read up to the file/byte budget.
|
|
171
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
172
|
+
const fs = require("node:fs");
|
|
173
|
+
const chunks: string[] = [];
|
|
174
|
+
let total = 0;
|
|
175
|
+
for (const { path: fPath } of candidates) {
|
|
176
|
+
if (chunks.length >= MAX_SESSION_FILES || total >= MAX_SESSION_BYTES) break;
|
|
177
|
+
try {
|
|
178
|
+
const fd = fs.openSync(fPath, "r");
|
|
179
|
+
const buf = Buffer.alloc(PER_FILE_BYTES);
|
|
180
|
+
const n = fs.readSync(fd, buf, 0, PER_FILE_BYTES, 0);
|
|
181
|
+
fs.closeSync(fd);
|
|
182
|
+
chunks.push(buf.toString("utf8", 0, n));
|
|
183
|
+
total += n;
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
161
186
|
return chunks;
|
|
162
187
|
}
|
|
163
188
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
|
|
6
|
+
import { collectUserPrompts } from "./trigger-gaps";
|
|
7
|
+
|
|
8
|
+
describe("collectUserPrompts budget (guards the dashboard-hang fix)", () => {
|
|
9
|
+
function fixture(promptCount = 50): string {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "cue-tg-"));
|
|
11
|
+
const proj = join(root, "proj-a");
|
|
12
|
+
mkdirSync(proj);
|
|
13
|
+
const lines = Array.from({ length: promptCount }, (_, i) =>
|
|
14
|
+
JSON.stringify({ type: "user", message: { role: "user", content: `prompt ${i}` } }));
|
|
15
|
+
writeFileSync(join(proj, "session.jsonl"), lines.join("\n"));
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("maxPrompts caps how many prompts are collected", () => {
|
|
20
|
+
const root = fixture(50);
|
|
21
|
+
try {
|
|
22
|
+
expect(collectUserPrompts(9999, root, { maxPrompts: 5 }).length).toBe(5);
|
|
23
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("maxBytes stops reading once the budget is exhausted", () => {
|
|
27
|
+
const root = fixture(50);
|
|
28
|
+
try {
|
|
29
|
+
// 1-byte budget reads at most the first (newest) file's prompts, never more
|
|
30
|
+
// than one file's worth — here there's a single file, so all 50, but a
|
|
31
|
+
// 0-byte-ish cap still returns the first file then stops (no unbounded walk).
|
|
32
|
+
const out = collectUserPrompts(9999, root, { maxBytes: 1 });
|
|
33
|
+
expect(out.length).toBeLessThanOrEqual(50);
|
|
34
|
+
expect(out.length).toBeGreaterThan(0);
|
|
35
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("unbounded default still collects every in-window prompt", () => {
|
|
39
|
+
const root = fixture(50);
|
|
40
|
+
try {
|
|
41
|
+
const out = collectUserPrompts(9999, root);
|
|
42
|
+
expect(out.length).toBe(50);
|
|
43
|
+
expect(out).toContain("prompt 0");
|
|
44
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("missing root returns empty, never throws", () => {
|
|
48
|
+
expect(collectUserPrompts(30, join(tmpdir(), "cue-does-not-exist-xyz"))).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -89,10 +89,33 @@ function resolveActiveProfile(explicit: string | null): string | null {
|
|
|
89
89
|
* each line is a message-shape object; we read defensively because the
|
|
90
90
|
* shape can vary by Claude Code version.
|
|
91
91
|
*/
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Bound the transcript scan so a large `~/.claude/projects/` (GBs of JSONL on
|
|
94
|
+
* an active machine) can't peg CPU / balloon memory / block the dashboard's
|
|
95
|
+
* single event loop. The detector is a recency heuristic, so reading the NEWEST
|
|
96
|
+
* transcripts under a byte + prompt budget keeps the signal while capping cost.
|
|
97
|
+
*/
|
|
98
|
+
export interface CollectOpts {
|
|
99
|
+
/** Max transcript bytes to read (newest-first). Default 32 MB. */
|
|
100
|
+
maxBytes?: number;
|
|
101
|
+
/** Max prompts to collect. Default 20,000. */
|
|
102
|
+
maxPrompts?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function collectUserPrompts(
|
|
106
|
+
sinceDays: number,
|
|
107
|
+
root = PROJECTS_ROOT,
|
|
108
|
+
opts: CollectOpts = {},
|
|
109
|
+
): string[] {
|
|
110
|
+
const maxBytes = opts.maxBytes ?? 32 * 1024 * 1024;
|
|
111
|
+
const maxPrompts = opts.maxPrompts ?? 20_000;
|
|
93
112
|
if (!existsSync(root)) return [];
|
|
94
113
|
const cutoff = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
|
|
95
|
-
|
|
114
|
+
|
|
115
|
+
// Gather in-window candidate files first, then read NEWEST-first under the
|
|
116
|
+
// budget — so when there's more history than the cap allows, we keep the most
|
|
117
|
+
// recent prompts (the ones that matter for "what should be firing lately").
|
|
118
|
+
const candidates: { fp: string; mt: number; size: number }[] = [];
|
|
96
119
|
let dirs: string[] = [];
|
|
97
120
|
try { dirs = readdirSync(root); } catch { return []; }
|
|
98
121
|
for (const dir of dirs) {
|
|
@@ -104,35 +127,46 @@ export function collectUserPrompts(sinceDays: number, root = PROJECTS_ROOT): str
|
|
|
104
127
|
try { files = readdirSync(dirPath).filter((f) => f.endsWith(".jsonl")); } catch { continue; }
|
|
105
128
|
for (const f of files) {
|
|
106
129
|
const fp = join(dirPath, f);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
try {
|
|
131
|
+
const st = statSync(fp);
|
|
132
|
+
if (st.mtimeMs < cutoff) continue;
|
|
133
|
+
candidates.push({ fp, mt: st.mtimeMs, size: st.size });
|
|
134
|
+
} catch { /* unreadable — skip */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
candidates.sort((a, b) => b.mt - a.mt);
|
|
138
|
+
|
|
139
|
+
const prompts: string[] = [];
|
|
140
|
+
let bytesRead = 0;
|
|
141
|
+
for (const c of candidates) {
|
|
142
|
+
if (bytesRead >= maxBytes || prompts.length >= maxPrompts) break;
|
|
143
|
+
bytesRead += c.size;
|
|
144
|
+
let raw = "";
|
|
145
|
+
try { raw = readFileSync(c.fp, "utf8"); } catch { continue; }
|
|
146
|
+
for (const line of raw.split("\n")) {
|
|
147
|
+
if (prompts.length >= maxPrompts) break;
|
|
148
|
+
if (!line.trim()) continue;
|
|
149
|
+
try {
|
|
150
|
+
const msg = JSON.parse(line) as {
|
|
151
|
+
type?: string;
|
|
152
|
+
role?: string;
|
|
153
|
+
message?: { role?: string; content?: unknown };
|
|
154
|
+
content?: unknown;
|
|
155
|
+
};
|
|
156
|
+
// Claude Code shape: { type: "user", message: { role: "user", content: "..." } }
|
|
157
|
+
const role = msg.role ?? msg.message?.role ?? msg.type;
|
|
158
|
+
if (role !== "user") continue;
|
|
159
|
+
const content = msg.message?.content ?? msg.content;
|
|
160
|
+
if (typeof content === "string") {
|
|
161
|
+
prompts.push(content);
|
|
162
|
+
} else if (Array.isArray(content)) {
|
|
163
|
+
for (const part of content) {
|
|
164
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
165
|
+
prompts.push(part.text);
|
|
132
166
|
}
|
|
133
167
|
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
168
|
+
}
|
|
169
|
+
} catch { /* skip malformed */ }
|
|
136
170
|
}
|
|
137
171
|
}
|
|
138
172
|
return prompts;
|
package/src/commands/update.ts
CHANGED
package/src/commands/validate.ts
CHANGED
|
@@ -19,6 +19,7 @@ const RESET = "\x1b[0m";
|
|
|
19
19
|
interface ParsedArgs {
|
|
20
20
|
all: boolean;
|
|
21
21
|
profile?: string;
|
|
22
|
+
online: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export async function run(args: string[]): Promise<number> {
|
|
@@ -33,9 +34,14 @@ export async function run(args: string[]): Promise<number> {
|
|
|
33
34
|
return 1;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// Offline by default: the npx fetchability check otherwise does one network
|
|
38
|
+
// `npx skills add` spawn per skill (thousands across --all, no cache), which
|
|
39
|
+
// hangs for minutes. `--online` opts back into the real fetch. An explicit
|
|
40
|
+
// CUE_OFFLINE=1 always wins (enforced inside the linter).
|
|
41
|
+
const lintOpts = { npxOffline: !parsed.online };
|
|
36
42
|
const results = parsed.all
|
|
37
|
-
? await lintAllProfiles()
|
|
38
|
-
: [await lintProfile(parsed.profile
|
|
43
|
+
? await lintAllProfiles(lintOpts)
|
|
44
|
+
: [await lintProfile(parsed.profile!, lintOpts)];
|
|
39
45
|
|
|
40
46
|
if (parsed.all && results.length === 0) {
|
|
41
47
|
process.stdout.write("No profiles found in profiles/.\n");
|
|
@@ -55,16 +61,17 @@ function parseArgs(args: string[]): ParsedArgs | "help" | string {
|
|
|
55
61
|
if (args.includes("-h") || args.includes("--help")) return "help";
|
|
56
62
|
|
|
57
63
|
const all = args.includes("--all");
|
|
64
|
+
const online = args.includes("--online") || args.includes("--no-offline");
|
|
58
65
|
const positional = args.filter((arg) => !arg.startsWith("-"));
|
|
59
66
|
|
|
60
67
|
if (all && positional.length > 0) {
|
|
61
68
|
return "cue validate: use either --all or <profile>, not both";
|
|
62
69
|
}
|
|
63
|
-
if (all) return { all: true };
|
|
70
|
+
if (all) return { all: true, online };
|
|
64
71
|
if (positional.length !== 1) {
|
|
65
72
|
return "cue validate: expected exactly one <profile>";
|
|
66
73
|
}
|
|
67
|
-
return { all: false, profile: positional[0] };
|
|
74
|
+
return { all: false, profile: positional[0], online };
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
function printHelp(stream: Pick<NodeJS.WriteStream, "write"> = process.stdout): void {
|
|
@@ -78,6 +85,11 @@ function printHelp(stream: Pick<NodeJS.WriteStream, "write"> = process.stdout):
|
|
|
78
85
|
" schema validity, inheritance, local/npx/plugin skill resolution, MCP registry resolution",
|
|
79
86
|
" W1-W5 warnings and E1-E3 lint errors",
|
|
80
87
|
"",
|
|
88
|
+
"Options:",
|
|
89
|
+
" --online Fetch uncached npx skills over the network (one `npx skills add`",
|
|
90
|
+
" per skill). Default is offline: uncached npx skills are reported",
|
|
91
|
+
" as 'not cached', not errors — keeps `--all` fast and hang-free.",
|
|
92
|
+
"",
|
|
81
93
|
].join("\n"),
|
|
82
94
|
);
|
|
83
95
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* On change: re-runs materialization with 500ms debounce.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { watch, existsSync,
|
|
8
|
+
import { watch, existsSync, } from "node:fs";
|
|
9
9
|
import { resolve, dirname, join } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import * as p from "@clack/prompts";
|
|
18
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync,
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { resolveProfileForCwd } from "../lib/cwd-resolver";
|