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.
Files changed (310) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +82 -33
  3. package/bin/cue-review-progress +107 -0
  4. package/bin/cue-review-watch +98 -0
  5. package/dist/cue.js +7352 -3744
  6. package/package.json +16 -5
  7. package/profiles/_types.ts +9 -0
  8. package/profiles/backend/profile.yaml +2 -0
  9. package/profiles/blog-writer/profile.yaml +10 -0
  10. package/profiles/browser/profile.yaml +9 -2
  11. package/profiles/builder/profile.yaml +3 -6
  12. package/profiles/career/profile.yaml +13 -2
  13. package/profiles/claude-api/profile.yaml +1 -1
  14. package/profiles/commerce/profile.yaml +27 -3
  15. package/profiles/core/logo.png +0 -0
  16. package/profiles/core/profile.yaml +62 -2
  17. package/profiles/dash-merge-test/profile.yaml +109 -0
  18. package/profiles/designer/profile.yaml +2 -0
  19. package/profiles/designer-medusa-next/profile.yaml +4 -1
  20. package/profiles/designer-medusa-vite/profile.yaml +4 -1
  21. package/profiles/docs-writer/profile.yaml +3 -1
  22. package/profiles/eu-tender-research/README.md +48 -0
  23. package/profiles/eu-tender-research/logo.png +0 -0
  24. package/profiles/eu-tender-research/profile.yaml +108 -0
  25. package/profiles/finance/logo.png +0 -0
  26. package/profiles/finance/profile.yaml +46 -0
  27. package/profiles/frontend/profile.yaml +5 -9
  28. package/profiles/growth/profile.yaml +2 -3
  29. package/profiles/gstack/profile.yaml +15 -0
  30. package/profiles/higgsfield/profile.yaml +3 -0
  31. package/profiles/hyperframes/logo.png +0 -0
  32. package/profiles/hyperframes/profile.yaml +59 -0
  33. package/profiles/improver/profile.yaml +88 -0
  34. package/profiles/marketing/profile.yaml +0 -3
  35. package/profiles/medusa-dev/profile.yaml +2 -0
  36. package/profiles/medusa-next/profile.yaml +2 -3
  37. package/profiles/medusa-vite/profile.yaml +2 -3
  38. package/profiles/n8n/logo.png +0 -0
  39. package/profiles/n8n/profile.yaml +50 -0
  40. package/profiles/nextjs/profile.yaml +2 -3
  41. package/profiles/ops/profile.yaml +2 -0
  42. package/profiles/postizz/profile.yaml +13 -3
  43. package/profiles/python/profile.yaml +3 -0
  44. package/profiles/research/profile.yaml +3 -1
  45. package/profiles/schema.json +10 -0
  46. package/profiles/secops/profile.yaml +2 -0
  47. package/profiles/seo/profile.yaml +56 -0
  48. package/profiles/skill-writer/profile.yaml +8 -0
  49. package/profiles/ssh/profile.yaml +32 -0
  50. package/profiles/strapi/logo.png +0 -0
  51. package/profiles/strapi/profile.yaml +45 -0
  52. package/profiles/stripe/logo.png +0 -0
  53. package/profiles/stripe/profile.yaml +1 -0
  54. package/profiles/supabase/logo.png +0 -0
  55. package/profiles/supabase/profile.yaml +85 -0
  56. package/profiles/vercel/logo.png +0 -0
  57. package/profiles/vercel/profile.yaml +25 -1
  58. package/profiles/vite/profile.yaml +4 -3
  59. package/profiles/web-frontend-base/profile.yaml +5 -4
  60. package/profiles/webshop/profile.yaml +23 -5
  61. package/profiles/x-growth-bot/profile.yaml +44 -0
  62. package/resources/icons/generate-icons.py +128 -2
  63. package/resources/mcps/configs/claude.sanitized.json +42 -0
  64. package/resources/mcps/configs/codex.sanitized.json +7 -0
  65. package/resources/skills/skills/career/resume-version-manager/SKILL.md +351 -0
  66. package/resources/skills/skills/career/salary-negotiation-prep/SKILL.md +378 -0
  67. package/resources/skills/skills/content/pdf/SKILL.md +2 -0
  68. package/resources/skills/skills/content/postiz-cards/SKILL.md +48 -0
  69. package/resources/skills/skills/content/postiz-cards/scripts/analytics.sh +38 -0
  70. package/resources/skills/skills/content/postiz-cards/scripts/card.sh +42 -0
  71. package/resources/skills/skills/content/postiz-cards/scripts/lint.py +38 -0
  72. package/resources/skills/skills/design/headless-gif-demo/SKILL.md +1 -1
  73. package/resources/skills/skills/design/readme-svg-design/SKILL.md +1 -1
  74. package/resources/skills/skills/eu-funding/grant-outreach/SKILL.md +70 -0
  75. package/resources/skills/skills/eu-funding/hu-grant-finder/SKILL.md +114 -0
  76. package/resources/skills/skills/eu-funding/hu-grant-finder/evals.md +26 -0
  77. package/resources/skills/skills/eu-funding/ted-tender-search/SKILL.md +80 -0
  78. package/resources/skills/skills/eu-funding/ted-tender-search/evals.md +26 -0
  79. package/resources/skills/skills/eu-funding/ted-tender-search/scripts/ted-search.sh +46 -0
  80. package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +1 -1
  81. package/resources/skills/skills/github/gx-agents/SKILL.md +96 -0
  82. package/resources/skills/skills/gstack/design-shotgun/SKILL.md +1 -1
  83. package/resources/skills/skills/marketing/ab-test-analyzer/SKILL.md +1 -1
  84. package/resources/skills/skills/marketing/ab-test-setup-and-analysis/SKILL.md +1 -1
  85. package/resources/skills/skills/marketing/account-structure-review/SKILL.md +1 -1
  86. package/resources/skills/skills/marketing/ad-copy-variant-generator/SKILL.md +1 -1
  87. package/resources/skills/skills/marketing/ad-extension-audit/SKILL.md +1 -1
  88. package/resources/skills/skills/marketing/ad-spend-allocator/SKILL.md +1 -1
  89. package/resources/skills/skills/marketing/anomaly-detection/SKILL.md +1 -1
  90. package/resources/skills/skills/marketing/attribution-model-comparison/SKILL.md +1 -1
  91. package/resources/skills/skills/marketing/audience-overlap-analysis/SKILL.md +7 -1
  92. package/resources/skills/skills/marketing/bid-strategy-recommendations/SKILL.md +7 -1
  93. package/resources/skills/skills/marketing/budget-scenario-planner/SKILL.md +6 -1
  94. package/resources/skills/skills/marketing/campaign-naming-convention-builder/SKILL.md +7 -1
  95. package/resources/skills/skills/marketing/channel-mix-optimizer/SKILL.md +7 -1
  96. package/resources/skills/skills/marketing/client-report-narratives/SKILL.md +6 -1
  97. package/resources/skills/skills/marketing/competitor-creative-analysis/SKILL.md +1 -1
  98. package/resources/skills/skills/marketing/competitor-teardown/SKILL.md +1 -1
  99. package/resources/skills/skills/marketing/content-repurposer/SKILL.md +1 -1
  100. package/resources/skills/skills/marketing/conversion-path-analysis/SKILL.md +1 -1
  101. package/resources/skills/skills/marketing/cpa-diagnostics/SKILL.md +1 -1
  102. package/resources/skills/skills/marketing/creative-fatigue-detection/SKILL.md +1 -1
  103. package/resources/skills/skills/marketing/day-hour-performance-breakdown/SKILL.md +1 -1
  104. package/resources/skills/skills/marketing/device-performance-split/SKILL.md +1 -1
  105. package/resources/skills/skills/marketing/e2e-seo-assistant/SKILL.md +1 -1
  106. package/resources/skills/skills/marketing/email-sequence-writer/SKILL.md +1 -1
  107. package/resources/skills/skills/marketing/frequency-cap-recommendations/SKILL.md +1 -1
  108. package/resources/skills/skills/marketing/geo-performance-analysis/SKILL.md +1 -1
  109. package/resources/skills/skills/marketing/google-ads-audit/SKILL.md +1 -1
  110. package/resources/skills/skills/marketing/icp-research-assistant/SKILL.md +1 -1
  111. package/resources/skills/skills/marketing/keyword-cannibalization-check/SKILL.md +1 -1
  112. package/resources/skills/skills/marketing/landing-page-audit/SKILL.md +1 -1
  113. package/resources/skills/skills/marketing/landing-page-audit-quick/SKILL.md +1 -1
  114. package/resources/skills/skills/marketing/linkedin-ads-audit/SKILL.md +1 -1
  115. package/resources/skills/skills/marketing/meta-ads-audit/SKILL.md +1 -1
  116. package/resources/skills/skills/marketing/pacing-monitor/SKILL.md +1 -1
  117. package/resources/skills/skills/marketing/performance-benchmarking/SKILL.md +1 -1
  118. package/resources/skills/skills/marketing/programmatic-seo-builder/SKILL.md +1 -1
  119. package/resources/skills/skills/marketing/quality-score-breakdown/SKILL.md +1 -1
  120. package/resources/skills/skills/marketing/reddit-ads-audit/SKILL.md +1 -1
  121. package/resources/skills/skills/marketing/retargeting-window-analysis/SKILL.md +1 -1
  122. package/resources/skills/skills/marketing/roas-forecasting/SKILL.md +1 -1
  123. package/resources/skills/skills/marketing/search-term-mining/SKILL.md +1 -1
  124. package/resources/skills/skills/marketing/utm-tracking-generator/SKILL.md +1 -1
  125. package/resources/skills/skills/marketing/wasted-spend-finder/SKILL.md +1 -1
  126. package/resources/skills/skills/marketing/weekly-account-summary/SKILL.md +1 -1
  127. package/resources/skills/skills/meta/awesome-list-submit/SKILL.md +4 -4
  128. package/resources/skills/skills/meta/cue-dashboard/SKILL.md +109 -0
  129. package/resources/skills/skills/meta/cue-developer/SKILL.md +161 -0
  130. package/resources/skills/skills/meta/cue-developer/evals/evals.json +57 -0
  131. package/resources/skills/skills/meta/cue-developer/references/architecture.md +65 -0
  132. package/resources/skills/skills/meta/cue-developer/references/build_and_test.md +72 -0
  133. package/resources/skills/skills/meta/cue-developer/references/contributing.md +75 -0
  134. package/resources/skills/skills/meta/cue-developer/references/conventions.md +57 -0
  135. package/resources/skills/skills/meta/cue-developer/references/first_time_setup.md +51 -0
  136. package/resources/skills/skills/meta/cue-developer/references/skill_and_mcp_authoring.md +84 -0
  137. package/resources/skills/skills/meta/cue-developer/references/troubleshooting.md +42 -0
  138. package/resources/skills/skills/meta/delegation-check/SKILL.md +148 -0
  139. package/resources/skills/skills/meta/delegation-check/specs/scan-algorithm.md +125 -0
  140. package/resources/skills/skills/meta/delegation-check/specs/separation-rules.md +190 -0
  141. package/resources/skills/skills/meta/focus/SKILL.md +62 -0
  142. package/resources/skills/skills/meta/help/SKILL.md +1 -1
  143. package/resources/skills/skills/meta/integrity-tags/SKILL.md +2 -0
  144. package/resources/skills/skills/meta/next-steps/SKILL.md +124 -0
  145. package/resources/skills/skills/meta/next-steps/evals/eval-set.json +92 -0
  146. package/resources/skills/skills/meta/profile-from-docs/SKILL.md +141 -0
  147. package/resources/skills/skills/meta/ralph-loop/SKILL.md +83 -0
  148. package/resources/skills/skills/meta/ralph-loop/scripts/loop.sh +73 -0
  149. package/resources/skills/skills/meta/skill-simplify/SKILL.md +136 -0
  150. package/resources/skills/skills/meta/skill-simplify/phases/01-analysis.md +173 -0
  151. package/resources/skills/skills/meta/skill-simplify/phases/02-optimize.md +104 -0
  152. package/resources/skills/skills/meta/skill-simplify/phases/03-check.md +145 -0
  153. package/resources/skills/skills/meta/smart-loader/scripts/smart-lookup.sh +13 -4
  154. package/resources/skills/skills/meta/verify-council/SKILL.md +182 -0
  155. package/resources/skills/skills/meta/verify-council/references/lane-prompts.md +103 -0
  156. package/resources/skills/skills/meta/verify-council/references/workflow.js +217 -0
  157. package/resources/skills/skills/nvidia/aiq-research/SKILL.md +1 -1
  158. package/resources/skills/skills/nvidia/cuopt-developer/SKILL.md +16 -1
  159. package/resources/skills/skills/nvidia/cuopt-developer/resources/contributing.md +2 -2
  160. package/resources/skills/skills/nvidia/cuopt-developer/resources/numerical_debugging.md +128 -0
  161. package/resources/skills/skills/nvidia/cuopt-developer/resources/python_bindings.md +2 -9
  162. package/resources/skills/skills/nvidia/cuopt-developer/resources/vrp_skills.md +166 -0
  163. package/resources/skills/skills/nvidia/cuopt-install/SKILL.md +2 -10
  164. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/SKILL.md +3 -23
  165. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/resources/examples.md +40 -20
  166. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-python/SKILL.md +5 -1
  167. package/resources/skills/skills/nvidia/skill-evolution/SKILL.md +4 -5
  168. package/resources/skills/skills/research/trendradar/SKILL.md +1 -1
  169. package/resources/skills/skills/ssh/ssh-config/SKILL.md +94 -0
  170. package/resources/skills/skills/ssh/ssh-copy/SKILL.md +92 -0
  171. package/resources/skills/skills/ssh/ssh-harden/SKILL.md +108 -0
  172. package/resources/skills/skills/ssh/ssh-keys/SKILL.md +82 -0
  173. package/resources/skills/skills/ssh/ssh-paste-image/LICENSE +28 -0
  174. package/resources/skills/skills/ssh/ssh-paste-image/SKILL.md +149 -0
  175. package/resources/skills/skills/ssh/ssh-paste-image/scripts/build.sh +29 -0
  176. package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/go.mod +3 -0
  177. package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/main.go +79 -0
  178. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/ccimgd.service +12 -0
  179. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/com.ccimgd.plist +20 -0
  180. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/go.mod +3 -0
  181. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/main.go +98 -0
  182. package/resources/skills/skills/ssh/ssh-tunnel/SKILL.md +96 -0
  183. package/resources/skills/skills/strapi/building-with-strapi/SKILL.md +112 -0
  184. package/resources/skills/skills/strapi/strapi-cli/SKILL.md +93 -0
  185. package/resources/skills/skills/strapi/strapi-content-api/SKILL.md +115 -0
  186. package/resources/skills/skills/strapi/strapi-deploy/SKILL.md +89 -0
  187. package/resources/skills/skills/strapi/strapi-mcp-setup/SKILL.md +101 -0
  188. package/resources/skills/skills/strapi/strapi-plugins/SKILL.md +97 -0
  189. package/resources/skills/skills/tools/context7/SKILL.md +101 -0
  190. package/resources/skills/skills/tools/opensrc/SKILL.md +1 -1
  191. package/resources/skills/skills/tools/portless/SKILL.md +186 -0
  192. package/resources/skills/skills/xbot/operate/SKILL.md +229 -0
  193. package/src/commands/_index.ts +8 -0
  194. package/src/commands/ai-score.e2e.test.ts +11 -4
  195. package/src/commands/ai.ts +3 -4
  196. package/src/commands/auto-detect.ts +1 -1
  197. package/src/commands/cli.test.ts +1 -2
  198. package/src/commands/cli.ts +1 -1
  199. package/src/commands/cloud.ts +1 -1
  200. package/src/commands/current.ts +1 -4
  201. package/src/commands/dash.test.ts +110 -0
  202. package/src/commands/dash.ts +194 -0
  203. package/src/commands/dashboard.ts +26 -0
  204. package/src/commands/diff.ts +1 -1
  205. package/src/commands/discover.test.ts +1 -1
  206. package/src/commands/discover.ts +90 -40
  207. package/src/commands/doctor.test.ts +58 -0
  208. package/src/commands/doctor.ts +79 -3
  209. package/src/commands/eval-behavior.ts +1 -1
  210. package/src/commands/eval.ts +2 -2
  211. package/src/commands/evolve.ts +4 -3
  212. package/src/commands/failures.test.ts +1 -1
  213. package/src/commands/features-batch1.test.ts +6 -1
  214. package/src/commands/icon.ts +1 -5
  215. package/src/commands/import-profile.ts +1 -1
  216. package/src/commands/init.ts +50 -7
  217. package/src/commands/install-sh.e2e.test.ts +65 -0
  218. package/src/commands/launch-handoff.e2e.test.ts +88 -0
  219. package/src/commands/launch.e2e.test.ts +8 -1
  220. package/src/commands/launch.test.ts +29 -0
  221. package/src/commands/launch.ts +185 -131
  222. package/src/commands/lock.ts +0 -1
  223. package/src/commands/marketplace.ts +0 -4
  224. package/src/commands/materialize.ts +1 -1
  225. package/src/commands/mem.ts +341 -0
  226. package/src/commands/optimizer.ts +0 -3
  227. package/src/commands/playground.ts +1 -2
  228. package/src/commands/profile-draft-skill.ts +1 -1
  229. package/src/commands/replay-whatif.ts +1 -6
  230. package/src/commands/score.ts +2 -2
  231. package/src/commands/security.test.ts +88 -0
  232. package/src/commands/security.ts +74 -28
  233. package/src/commands/shell.test.ts +65 -4
  234. package/src/commands/shell.ts +67 -7
  235. package/src/commands/skills-test.ts +0 -1
  236. package/src/commands/skills.ts +28 -2
  237. package/src/commands/sources.ts +1 -2
  238. package/src/commands/status.ts +2 -6
  239. package/src/commands/submit-profile.ts +1 -1
  240. package/src/commands/suggest.ts +35 -10
  241. package/src/commands/trigger-gaps.test.ts +50 -0
  242. package/src/commands/trigger-gaps.ts +63 -29
  243. package/src/commands/update.ts +1 -1
  244. package/src/commands/validate.ts +16 -4
  245. package/src/commands/watch-live.ts +1 -1
  246. package/src/commands/workspace.ts +1 -1
  247. package/src/index.ts +26 -10
  248. package/src/lib/active-sessions.ts +1 -1
  249. package/src/lib/agent-adapters.test.ts +100 -0
  250. package/src/lib/agent-adapters.ts +2 -2
  251. package/src/lib/analytics.test.ts +88 -0
  252. package/src/lib/analytics.ts +82 -1
  253. package/src/lib/auto-detect.test.ts +10 -4
  254. package/src/lib/auto-detect.ts +19 -23
  255. package/src/lib/brand-icons.ts +0 -1
  256. package/src/lib/cache.ts +2 -3
  257. package/src/lib/claude-mem-env.test.ts +148 -0
  258. package/src/lib/claude-mem-env.ts +172 -0
  259. package/src/lib/combo-history.test.ts +53 -0
  260. package/src/lib/combo-history.ts +83 -0
  261. package/src/lib/companion-detect.test.ts +108 -0
  262. package/src/lib/companion-detect.ts +140 -0
  263. package/src/lib/companion-fetch.ts +4 -6
  264. package/src/lib/conditional-skills.test.ts +1 -1
  265. package/src/lib/config-paths.test.ts +53 -0
  266. package/src/lib/config-paths.ts +33 -0
  267. package/src/lib/dashboard-server.test.ts +351 -0
  268. package/src/lib/dashboard-server.ts +1476 -27
  269. package/src/lib/debug-log.test.ts +66 -0
  270. package/src/lib/debug-log.ts +45 -0
  271. package/src/lib/mcp-catalog.test.ts +102 -0
  272. package/src/lib/mcp-catalog.ts +193 -0
  273. package/src/lib/pair-suggestions.test.ts +111 -0
  274. package/src/lib/pair-suggestions.ts +98 -5
  275. package/src/lib/permissions.test.ts +76 -0
  276. package/src/lib/permissions.ts +125 -0
  277. package/src/lib/picker.test.ts +1106 -1
  278. package/src/lib/picker.ts +1230 -142
  279. package/src/lib/plugin-discovery.ts +126 -0
  280. package/src/lib/pr-poster.ts +1 -1
  281. package/src/lib/pr-throttle.ts +2 -6
  282. package/src/lib/profile-linter.test.ts +67 -1
  283. package/src/lib/profile-linter.ts +59 -14
  284. package/src/lib/profile-loader.test.ts +21 -0
  285. package/src/lib/profile-loader.ts +22 -3
  286. package/src/lib/profile-metrics.ts +2 -6
  287. package/src/lib/profile-names.test.ts +58 -0
  288. package/src/lib/repos.test.ts +57 -0
  289. package/src/lib/repos.ts +167 -0
  290. package/src/lib/resolver-npx.ts +10 -1
  291. package/src/lib/runtime-materializer.test.ts +200 -3
  292. package/src/lib/runtime-materializer.ts +129 -20
  293. package/src/lib/shared-profiles.ts +2 -3
  294. package/src/lib/skill-clis.test.ts +113 -0
  295. package/src/lib/skill-clis.ts +232 -0
  296. package/src/lib/skill-dependencies.ts +9 -1
  297. package/src/lib/skill-deps.ts +1 -1
  298. package/src/lib/skill-linter.ts +1 -1
  299. package/src/lib/skill-quality.ts +0 -1
  300. package/src/lib/skill-sandbox.test.ts +1 -1
  301. package/src/lib/skills-lock.test.ts +1 -1
  302. package/src/lib/telemetry-consent.ts +3 -5
  303. package/src/lib/telemetry-report.test.ts +2 -2
  304. package/src/lib/token-budget.ts +111 -0
  305. package/src/lib/trigger-gaps.test.ts +70 -0
  306. package/src/lib/trigger-gaps.ts +48 -6
  307. package/src/lib/tui/data.ts +1 -5
  308. package/src/lib/workflow-store.ts +150 -0
  309. package/src/lib/workspace-secrets.ts +0 -4
  310. 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("exec cue launch claude");
29
+ expect(claudeShim).toContain("launch claude");
27
30
  const codexShim = await readFile(join(fakeHome, ".local", "bin", "codex"), "utf8");
28
- expect(codexShim).toContain("exec cue launch codex");
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
+ });
@@ -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 cueBin = resolve(process.env.CUE_REPO_ROOT ?? join(home, "Documents", "cue"), "bin", "cue");
172
+ const cueInvoke = resolveCueInvocation();
115
173
 
116
- const claudeShim = `#!/usr/bin/env bash\nexec cue launch claude "$@"\n`;
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 cue launch codex "$@"\n`;
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
- const cueBin = resolve(process.env.CUE_REPO_ROOT ?? join(homedir(), "Documents", "cue"), "bin", "cue");
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 "${cueBin}" launch claude "$@"
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 "${cueBin}" launch codex "$@"
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?
@@ -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
- const passedArgs = [...args];
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");
@@ -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 (e) {
99
+ try { profile = await loadProfile(profileName); } catch {
101
100
  process.stderr.write(`Profile "${profileName}" not found.\n`);
102
101
  return 1;
103
102
  }
@@ -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, lstatSync } from "node:fs";
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, basename, resolve } from "node:path";
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";
@@ -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, listProfiles } from "../lib/profile-loader";
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
- const chunks: string[] = [];
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
- const files = readdirSync(dirPath).filter(f => f.endsWith(".jsonl"));
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
- // Read first 100KB of each file
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
- export function collectUserPrompts(sinceDays: number, root = PROJECTS_ROOT): string[] {
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
- const prompts: string[] = [];
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
- let mt = 0;
108
- try { mt = statSync(fp).mtimeMs; } catch { continue; }
109
- if (mt < cutoff) continue;
110
- let raw = "";
111
- try { raw = readFileSync(fp, "utf8"); } catch { continue; }
112
- for (const line of raw.split("\n")) {
113
- if (!line.trim()) continue;
114
- try {
115
- const msg = JSON.parse(line) as {
116
- type?: string;
117
- role?: string;
118
- message?: { role?: string; content?: unknown };
119
- content?: unknown;
120
- };
121
- // Claude Code shape: { type: "user", message: { role: "user", content: "..." } }
122
- const role = msg.role ?? msg.message?.role ?? msg.type;
123
- if (role !== "user") continue;
124
- const content = msg.message?.content ?? msg.content;
125
- if (typeof content === "string") {
126
- prompts.push(content);
127
- } else if (Array.isArray(content)) {
128
- for (const part of content) {
129
- if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
130
- prompts.push(part.text);
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
- } catch { /* skip malformed */ }
135
- }
168
+ }
169
+ } catch { /* skip malformed */ }
136
170
  }
137
171
  }
138
172
  return prompts;
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawnSync } from "node:child_process";
6
- import { existsSync, readFileSync } from "node:fs";
6
+ import { existsSync, } from "node:fs";
7
7
  import { resolve, dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
@@ -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, readFileSync } from "node:fs";
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, mkdirSync } from "node:fs";
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";