cue-ai 0.9.2 → 0.9.3

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 (278) hide show
  1. package/CHANGELOG.md +4 -3
  2. package/README.md +148 -170
  3. package/bin/cue-learnings +30 -4
  4. package/bin/cue-review-progress +0 -0
  5. package/bin/cue-review-watch +0 -0
  6. package/dist/cue.js +4328 -3108
  7. package/package.json +1 -1
  8. package/plugins/cue/commands/cue-switch.md +1 -1
  9. package/plugins/cue/commands/cue.md +1 -1
  10. package/profiles/backend/profile.yaml +4 -0
  11. package/profiles/browser/profile.yaml +4 -0
  12. package/profiles/career/profile.yaml +2 -13
  13. package/profiles/commerce/profile.yaml +0 -2
  14. package/profiles/coolify/profile.yaml +0 -1
  15. package/profiles/core/profile.yaml +78 -11
  16. package/profiles/dash-merge-test/profile.yaml +6 -1
  17. package/profiles/designer/profile.yaml +9 -1
  18. package/profiles/dropshipping/profile.yaml +69 -0
  19. package/profiles/frontend/profile.yaml +4 -0
  20. package/profiles/google-ads/profile.yaml +34 -0
  21. package/profiles/google-analytics/profile.yaml +34 -0
  22. package/profiles/google-drive/profile.yaml +34 -0
  23. package/profiles/gstack/profile.yaml +117 -29
  24. package/profiles/marketing/profile.yaml +0 -1
  25. package/profiles/media/README.md +70 -0
  26. package/profiles/media/profile.yaml +104 -0
  27. package/profiles/nano-banana/profile.yaml +52 -0
  28. package/profiles/ops/profile.yaml +1 -2
  29. package/profiles/secops/profile.yaml +3 -0
  30. package/profiles/skill-writer/profile.yaml +15 -0
  31. package/profiles/video/profile.yaml +3 -0
  32. package/profiles/web-frontend-base/profile.yaml +6 -0
  33. package/profiles/webshop/profile.yaml +0 -1
  34. package/profiles/webshop-google/profile.yaml +1 -0
  35. package/profiles/x-growth-bot/profile.yaml +2 -0
  36. package/resources/icons/generate-icons.py +2 -128
  37. package/resources/mcps/configs/claude.sanitized.json +88 -20
  38. package/resources/mcps/configs/claude_runtime.sanitized.json +40 -1
  39. package/resources/mcps/configs/codex.sanitized.json +29 -0
  40. package/resources/skills/skills/career/job-hunter/LICENSE +21 -0
  41. package/resources/skills/skills/career/job-hunter/README.md +323 -0
  42. package/resources/skills/skills/career/job-hunter/SKILL.md +91 -0
  43. package/resources/skills/skills/career/job-hunter/agents/README.md +96 -0
  44. package/resources/skills/skills/career/job-hunter/agents/apply-assessment-prep.md +195 -0
  45. package/resources/skills/skills/career/job-hunter/agents/apply-ats-scan.md +155 -0
  46. package/resources/skills/skills/career/job-hunter/agents/apply-bias-audit.md +224 -0
  47. package/resources/skills/skills/career/job-hunter/agents/apply-cover-letter.md +69 -0
  48. package/resources/skills/skills/career/job-hunter/agents/apply-decode-jd.md +117 -0
  49. package/resources/skills/skills/career/job-hunter/agents/apply-fit-score.md +183 -0
  50. package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-audit.md +74 -0
  51. package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-scrape.md +255 -0
  52. package/resources/skills/skills/career/job-hunter/agents/apply-portfolio-brief.md +123 -0
  53. package/resources/skills/skills/career/job-hunter/agents/apply-reality-check.md +164 -0
  54. package/resources/skills/skills/career/job-hunter/agents/apply-reference-prep.md +150 -0
  55. package/resources/skills/skills/career/job-hunter/agents/apply-rejection-analysis.md +172 -0
  56. package/resources/skills/skills/career/job-hunter/agents/apply-resume.md +70 -0
  57. package/resources/skills/skills/career/job-hunter/agents/apply-skills-gap-filler.md +109 -0
  58. package/resources/skills/skills/career/job-hunter/agents/career-internal.md +94 -0
  59. package/resources/skills/skills/career/job-hunter/agents/career-linkedin-content.md +173 -0
  60. package/resources/skills/skills/career/job-hunter/agents/career-linkedin-scanner.md +262 -0
  61. package/resources/skills/skills/career/job-hunter/agents/career-network-message.md +108 -0
  62. package/resources/skills/skills/career/job-hunter/agents/career-promote.md +102 -0
  63. package/resources/skills/skills/career/job-hunter/agents/career-review.md +71 -0
  64. package/resources/skills/skills/career/job-hunter/agents/interview-debrief.md +117 -0
  65. package/resources/skills/skills/career/job-hunter/agents/interview-mock.md +171 -0
  66. package/resources/skills/skills/career/job-hunter/agents/interview-panel-decoder.md +152 -0
  67. package/resources/skills/skills/career/job-hunter/agents/interview-prep.md +184 -0
  68. package/resources/skills/skills/career/job-hunter/agents/interview-question-bank.md +133 -0
  69. package/resources/skills/skills/career/job-hunter/agents/interview-research.md +148 -0
  70. package/resources/skills/skills/career/job-hunter/agents/offer-compare.md +117 -0
  71. package/resources/skills/skills/career/job-hunter/agents/offer-counteroffer.md +144 -0
  72. package/resources/skills/skills/career/job-hunter/agents/offer-deadline-manager.md +148 -0
  73. package/resources/skills/skills/career/job-hunter/agents/offer-negotiate.md +126 -0
  74. package/resources/skills/skills/career/job-hunter/agents/offer-schedule.md +99 -0
  75. package/resources/skills/skills/career/job-hunter/agents/offer-thankyou.md +80 -0
  76. package/resources/skills/skills/career/job-hunter/agents/search-company-research.md +146 -0
  77. package/resources/skills/skills/career/job-hunter/agents/search-follow-up.md +129 -0
  78. package/resources/skills/skills/career/job-hunter/agents/search-ghost-job-detector.md +152 -0
  79. package/resources/skills/skills/career/job-hunter/agents/search-inbox-scan.md +193 -0
  80. package/resources/skills/skills/career/job-hunter/agents/search-interview-scorecard.md +164 -0
  81. package/resources/skills/skills/career/job-hunter/agents/search-jobs.md +149 -0
  82. package/resources/skills/skills/career/job-hunter/agents/search-momentum-check.md +194 -0
  83. package/resources/skills/skills/career/job-hunter/agents/search-outreach.md +85 -0
  84. package/resources/skills/skills/career/job-hunter/agents/search-referral-finder.md +124 -0
  85. package/resources/skills/skills/career/job-hunter/agents/search-salary.md +96 -0
  86. package/resources/skills/skills/career/job-hunter/agents/search-send-email.md +109 -0
  87. package/resources/skills/skills/career/job-hunter/agents/search-tracker-update.md +127 -0
  88. package/resources/skills/skills/career/job-hunter/inputs/README.md +26 -0
  89. package/resources/skills/skills/career/job-hunter/inputs/apply-linkedin-url.txt +8 -0
  90. package/resources/skills/skills/career/job-hunter/inputs/interview-context.md +24 -0
  91. package/resources/skills/skills/career/job-hunter/inputs/job-description.md +20 -0
  92. package/resources/skills/skills/career/job-hunter/inputs/job-search-criteria.md +36 -0
  93. package/resources/skills/skills/career/job-hunter/inputs/my-linkedin.md +24 -0
  94. package/resources/skills/skills/career/job-hunter/inputs/my-resume.md +28 -0
  95. package/resources/skills/skills/career/job-hunter/inputs/search-outreach-target.md +24 -0
  96. package/resources/skills/skills/career/job-hunter/rules/README.md +37 -0
  97. package/resources/skills/skills/career/job-hunter/rules/writing-rules.md +81 -0
  98. package/resources/skills/skills/design/banana/SKILL.md +375 -0
  99. package/resources/skills/skills/design/banana/references/cost-tracking.md +47 -0
  100. package/resources/skills/skills/design/banana/references/gemini-models.md +236 -0
  101. package/resources/skills/skills/design/banana/references/mcp-tools.md +145 -0
  102. package/resources/skills/skills/design/banana/references/post-processing.md +192 -0
  103. package/resources/skills/skills/design/banana/references/presets.md +69 -0
  104. package/resources/skills/skills/design/banana/references/prompt-engineering.md +481 -0
  105. package/resources/skills/skills/design/banana/scripts/batch.py +97 -0
  106. package/resources/skills/skills/design/banana/scripts/cost_tracker.py +191 -0
  107. package/resources/skills/skills/design/banana/scripts/edit.py +159 -0
  108. package/resources/skills/skills/design/banana/scripts/generate.py +168 -0
  109. package/resources/skills/skills/design/banana/scripts/presets.py +154 -0
  110. package/resources/skills/skills/design/banana/scripts/setup_mcp.py +151 -0
  111. package/resources/skills/skills/design/banana/scripts/validate_setup.py +133 -0
  112. package/resources/skills/skills/gstack/ship/SKILL.md +13 -0
  113. package/resources/skills/skills/media/3d-logo-animation/SKILL.md +59 -0
  114. package/resources/skills/skills/media/action-figure-generator/SKILL.md +48 -0
  115. package/resources/skills/skills/media/ad-creative/SKILL.md +79 -0
  116. package/resources/skills/skills/media/ai-clipping/SKILL.md +194 -0
  117. package/resources/skills/skills/media/ai-clipping/scripts/run-ai-clipping.sh +200 -0
  118. package/resources/skills/skills/media/ai-fight-scene/SKILL.md +132 -0
  119. package/resources/skills/skills/media/amazon-product-listing/SKILL.md +68 -0
  120. package/resources/skills/skills/media/animal-video-generator/SKILL.md +59 -0
  121. package/resources/skills/skills/media/award-ceremony-video/SKILL.md +87 -0
  122. package/resources/skills/skills/media/blog-header/SKILL.md +61 -0
  123. package/resources/skills/skills/media/brand-kit/SKILL.md +72 -0
  124. package/resources/skills/skills/media/brochures/SKILL.md +65 -0
  125. package/resources/skills/skills/media/cartoon-dance-animation/SKILL.md +62 -0
  126. package/resources/skills/skills/media/character-story-video/SKILL.md +84 -0
  127. package/resources/skills/skills/media/chibi-collage-effect/SKILL.md +63 -0
  128. package/resources/skills/skills/media/cinema-director/SKILL.md +93 -0
  129. package/resources/skills/skills/media/cinema-director/scripts/generate-film.sh +78 -0
  130. package/resources/skills/skills/media/color-analysis-board/SKILL.md +71 -0
  131. package/resources/skills/skills/media/core-edit/SKILL.md +48 -0
  132. package/resources/skills/skills/media/core-edit/edit-image.sh +54 -0
  133. package/resources/skills/skills/media/core-edit/enhance-image.sh +191 -0
  134. package/resources/skills/skills/media/core-edit/lipsync.sh +144 -0
  135. package/resources/skills/skills/media/core-edit/video-effects.sh +193 -0
  136. package/resources/skills/skills/media/core-media/SKILL.md +49 -0
  137. package/resources/skills/skills/media/core-media/create-music.sh +169 -0
  138. package/resources/skills/skills/media/core-media/generate-image.sh +161 -0
  139. package/resources/skills/skills/media/core-media/generate-video.sh +137 -0
  140. package/resources/skills/skills/media/core-media/image-to-video.sh +228 -0
  141. package/resources/skills/skills/media/core-media/schema_data.json +18708 -0
  142. package/resources/skills/skills/media/core-media/upload.sh +41 -0
  143. package/resources/skills/skills/media/core-platform/SKILL.md +41 -0
  144. package/resources/skills/skills/media/core-platform/check-result.sh +37 -0
  145. package/resources/skills/skills/media/core-platform/setup.sh +31 -0
  146. package/resources/skills/skills/media/couple-grid-creator/SKILL.md +47 -0
  147. package/resources/skills/skills/media/design-guide/SKILL.md +73 -0
  148. package/resources/skills/skills/media/drone-style-video/SKILL.md +61 -0
  149. package/resources/skills/skills/media/fashion-try-on/SKILL.md +61 -0
  150. package/resources/skills/skills/media/floor-plan-rendering/SKILL.md +56 -0
  151. package/resources/skills/skills/media/freeze-effect-video/SKILL.md +100 -0
  152. package/resources/skills/skills/media/giant-product-showcase/SKILL.md +61 -0
  153. package/resources/skills/skills/media/instagram-post/SKILL.md +58 -0
  154. package/resources/skills/skills/media/interior-design/SKILL.md +61 -0
  155. package/resources/skills/skills/media/interior-design-visualizer/SKILL.md +57 -0
  156. package/resources/skills/skills/media/jewelry-product-video/SKILL.md +61 -0
  157. package/resources/skills/skills/media/kdenlive/SKILL.md +106 -0
  158. package/resources/skills/skills/media/kdenlive/scripts/assemble.sh +57 -0
  159. package/resources/skills/skills/media/kdenlive/scripts/common.sh +30 -0
  160. package/resources/skills/skills/media/kdenlive/scripts/inspect.sh +19 -0
  161. package/resources/skills/skills/media/kdenlive/scripts/reframe.sh +22 -0
  162. package/resources/skills/skills/media/kdenlive/scripts/render.sh +16 -0
  163. package/resources/skills/skills/media/kdenlive/scripts/title-card.sh +25 -0
  164. package/resources/skills/skills/media/keyboard-art-maker/SKILL.md +44 -0
  165. package/resources/skills/skills/media/logo-branding/SKILL.md +70 -0
  166. package/resources/skills/skills/media/logo-creator/SKILL.md +80 -0
  167. package/resources/skills/skills/media/logo-creator/scripts/create-logo.sh +38 -0
  168. package/resources/skills/skills/media/logo-generator/SKILL.md +56 -0
  169. package/resources/skills/skills/media/multi-angle-reshoot/SKILL.md +70 -0
  170. package/resources/skills/skills/media/multi-angle-shots/SKILL.md +73 -0
  171. package/resources/skills/skills/media/music-video/SKILL.md +61 -0
  172. package/resources/skills/skills/media/nano-banana/SKILL.md +80 -0
  173. package/resources/skills/skills/media/nano-banana/scripts/generate-nano-art.sh +54 -0
  174. package/resources/skills/skills/media/one-shot-video/SKILL.md +56 -0
  175. package/resources/skills/skills/media/photo-pack-generator/SKILL.md +205 -0
  176. package/resources/skills/skills/media/photo-pack-generator/scripts/generate-pack.sh +241 -0
  177. package/resources/skills/skills/media/product-ad-cinematic/SKILL.md +78 -0
  178. package/resources/skills/skills/media/product-campaign/SKILL.md +76 -0
  179. package/resources/skills/skills/media/product-showcase-video/SKILL.md +60 -0
  180. package/resources/skills/skills/media/product-video-ad-maker/SKILL.md +59 -0
  181. package/resources/skills/skills/media/rednote-cover/SKILL.md +57 -0
  182. package/resources/skills/skills/media/seedance-2/SKILL.md +632 -0
  183. package/resources/skills/skills/media/seedance-2/scripts/generate-seedance.sh +701 -0
  184. package/resources/skills/skills/media/selfie-with-celebrities/SKILL.md +64 -0
  185. package/resources/skills/skills/media/social-media-video/SKILL.md +277 -0
  186. package/resources/skills/skills/media/social-media-video/scripts/run-social-video.sh +316 -0
  187. package/resources/skills/skills/media/social-pack/SKILL.md +58 -0
  188. package/resources/skills/skills/media/storyboard/SKILL.md +57 -0
  189. package/resources/skills/skills/media/storyboard-to-cooking-video/SKILL.md +143 -0
  190. package/resources/skills/skills/media/talking-baby-video/SKILL.md +57 -0
  191. package/resources/skills/skills/media/ugc-ads-workflow/SKILL.md +70 -0
  192. package/resources/skills/skills/media/ugc-lifestyle-try-on/SKILL.md +65 -0
  193. package/resources/skills/skills/media/ugc-video-factory/SKILL.md +134 -0
  194. package/resources/skills/skills/media/ui-design/SKILL.md +81 -0
  195. package/resources/skills/skills/media/ui-design/scripts/generate-mockup.sh +49 -0
  196. package/resources/skills/skills/media/url-to-design/SKILL.md +61 -0
  197. package/resources/skills/skills/media/workflow/SKILL.md +197 -0
  198. package/resources/skills/skills/media/workflow/scripts/discover-workflow.sh +18 -0
  199. package/resources/skills/skills/media/workflow/scripts/generate-workflow.sh +33 -0
  200. package/resources/skills/skills/media/workflow/scripts/interactive-run.sh +16 -0
  201. package/resources/skills/skills/media/workflow/scripts/list-workflows.sh +20 -0
  202. package/resources/skills/skills/media/workflow/scripts/run-workflow.sh +34 -0
  203. package/resources/skills/skills/media/youtube-shorts/SKILL.md +173 -0
  204. package/resources/skills/skills/media/youtube-shorts/scripts/run-youtube-shorts.sh +141 -0
  205. package/resources/skills/skills/media/youtube-thumbnail/SKILL.md +66 -0
  206. package/resources/skills/skills/meta/cue-developer/references/architecture.md +2 -2
  207. package/resources/skills/skills/meta/cue-usage/SKILL.md +1 -1
  208. package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +2 -2
  209. package/resources/skills/skills/meta/profile-optimizer/SKILL.md +1 -1
  210. package/resources/skills/skills/meta/profile-suggest/SKILL.md +7 -7
  211. package/resources/skills/skills/meta/profile-summon/SKILL.md +159 -0
  212. package/resources/skills/skills/meta/profile-summon/evals/evals.json +53 -0
  213. package/resources/skills/skills/meta/save-profile/SKILL.md +1 -1
  214. package/resources/skills/skills/meta/skill-reviewer/SKILL.md +3 -0
  215. package/resources/skills/skills/meta/skill-reviewer/references/tdd-for-skills.md +55 -0
  216. package/resources/skills/skills/research/find-skills/SKILL.md +1 -1
  217. package/resources/skills/skills/review/code-review-deep/SKILL.md +20 -0
  218. package/resources/skills/skills/security/trivy-scan/SKILL.md +139 -0
  219. package/resources/skills/skills/security/trivy-scan/scripts/ensure-trivy.sh +21 -0
  220. package/resources/skills/skills/tools/ccusage/SKILL.md +142 -0
  221. package/src/commands/_index.ts +8 -0
  222. package/src/commands/ai.ts +2 -2
  223. package/src/commands/auto-detect.test.ts +74 -0
  224. package/src/commands/auto-detect.ts +9 -7
  225. package/src/commands/cli.test.ts +20 -4
  226. package/src/commands/cli.ts +36 -20
  227. package/src/commands/create-profile.ts +2 -2
  228. package/src/commands/debug.ts +2 -2
  229. package/src/commands/discover.ts +14 -4
  230. package/src/commands/export-docker.ts +1 -1
  231. package/src/commands/features-batch1.test.ts +1 -1
  232. package/src/commands/gates.ts +1 -1
  233. package/src/commands/import-profile.ts +1 -1
  234. package/src/commands/init.ts +15 -11
  235. package/src/commands/install.test.ts +192 -0
  236. package/src/commands/install.ts +610 -0
  237. package/src/commands/launch-handoff.e2e.test.ts +33 -1
  238. package/src/commands/launch.e2e.test.ts +15 -10
  239. package/src/commands/launch.ts +73 -116
  240. package/src/commands/materialize.ts +2 -2
  241. package/src/commands/prune.ts +1 -1
  242. package/src/commands/security-audit.ts +1 -1
  243. package/src/commands/shell.ts +7 -7
  244. package/src/commands/skill-report.ts +1 -1
  245. package/src/commands/skills.ts +3 -3
  246. package/src/commands/snapshot.ts +2 -2
  247. package/src/commands/summon.test.ts +116 -0
  248. package/src/commands/summon.ts +338 -0
  249. package/src/commands/trigger-gaps.ts +1 -1
  250. package/src/commands/use.ts +47 -3
  251. package/src/commands/watch-live.ts +5 -5
  252. package/src/commands/watch.ts +8 -8
  253. package/src/index.ts +2 -0
  254. package/src/lib/active-sessions.test.ts +3 -3
  255. package/src/lib/active-sessions.ts +4 -4
  256. package/src/lib/auto-detect.test.ts +172 -8
  257. package/src/lib/auto-detect.ts +191 -136
  258. package/src/lib/codex-persona-parity.test.ts +58 -0
  259. package/src/lib/companion-detect.test.ts +43 -1
  260. package/src/lib/companion-detect.ts +35 -0
  261. package/src/lib/credentials-sync.test.ts +121 -1
  262. package/src/lib/credentials-sync.ts +95 -1
  263. package/src/lib/cwd-resolver.test.ts +8 -8
  264. package/src/lib/cwd-resolver.ts +2 -2
  265. package/src/lib/dashboard-merge.test.ts +9 -4
  266. package/src/lib/dashboard-server.ts +1 -1
  267. package/src/lib/picker.test.ts +1 -1
  268. package/src/lib/picker.ts +5 -5
  269. package/src/lib/profile-merge.test.ts +8 -0
  270. package/src/lib/profile-names.test.ts +3 -3
  271. package/src/lib/runtime-install.ts +166 -0
  272. package/src/lib/runtime-materializer.test.ts +137 -0
  273. package/src/lib/runtime-materializer.ts +105 -2
  274. package/src/lib/skill-router.test.ts +38 -0
  275. package/src/lib/skill-router.ts +65 -4
  276. package/profiles/eu-tender-research/README.md +0 -48
  277. package/profiles/eu-tender-research/logo.png +0 -0
  278. package/profiles/eu-tender-research/profile.yaml +0 -108
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
6
- import { detectProfile, detectProfileV2 } from "./auto-detect";
6
+ import { detectProfileV2 } from "./auto-detect";
7
7
 
8
8
  let tmp: string;
9
9
 
@@ -116,6 +116,73 @@ describe("detectProfileV2", () => {
116
116
  for (const r of results) expect(r.confidence).toBeLessThanOrEqual(0.97);
117
117
  });
118
118
 
119
+ test("package.json with stripe dep → stripe profile suggested", () => {
120
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
121
+ dependencies: { stripe: "14.0.0" },
122
+ }));
123
+ const results = detectProfileV2(tmp);
124
+ const stripe = results.find(r => r.profile === "stripe");
125
+ expect(stripe).toBeDefined();
126
+ // Above the picker's SUGGESTED_MIN_CONFIDENCE (0.5) so it actually shows,
127
+ // below SUGGESTED_AUTO_PICK_CONFIDENCE (0.7) so it never hijacks Enter.
128
+ expect(stripe!.confidence).toBeGreaterThanOrEqual(0.5);
129
+ expect(stripe!.confidence).toBeLessThan(0.7);
130
+ expect(stripe!.reasons.join(" ")).toContain("stripe");
131
+ });
132
+
133
+ test("package.json with @aws-sdk/client-s3 → aws profile suggested", () => {
134
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
135
+ dependencies: { "@aws-sdk/client-s3": "3.0.0" },
136
+ }));
137
+ const results = detectProfileV2(tmp);
138
+ const aws = results.find(r => r.profile === "aws");
139
+ expect(aws).toBeDefined();
140
+ expect(aws!.confidence).toBeGreaterThanOrEqual(0.5);
141
+ });
142
+
143
+ test("service deps ride alongside the framework profile, not above it", () => {
144
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
145
+ dependencies: { next: "14.0.0", stripe: "14.0.0" },
146
+ }));
147
+ const results = detectProfileV2(tmp);
148
+ const nextjs = results.find(r => r.profile === "nextjs");
149
+ const stripe = results.find(r => r.profile === "stripe");
150
+ expect(nextjs).toBeDefined();
151
+ expect(stripe).toBeDefined();
152
+ expect(nextjs!.confidence).toBeGreaterThan(stripe!.confidence);
153
+ });
154
+
155
+ test("scoped service deps match by prefix (@supabase/, @slack/)", () => {
156
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
157
+ dependencies: { "@supabase/supabase-js": "2.0.0" },
158
+ devDependencies: { "@slack/web-api": "7.0.0" },
159
+ }));
160
+ const results = detectProfileV2(tmp);
161
+ expect(results.find(r => r.profile === "supabase")).toBeDefined();
162
+ expect(results.find(r => r.profile === "slack")).toBeDefined();
163
+ });
164
+
165
+ test("no service dep → no service profile suggested", () => {
166
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
167
+ dependencies: { express: "4.0.0" },
168
+ }));
169
+ const results = detectProfileV2(tmp);
170
+ expect(results.find(r => r.profile === "stripe")).toBeUndefined();
171
+ expect(results.find(r => r.profile === "aws")).toBeUndefined();
172
+ });
173
+
174
+ test("react-native dep → react-native profile outranks generic frontend", () => {
175
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
176
+ dependencies: { react: "18.0.0", "react-native": "0.74.0" },
177
+ }));
178
+ const results = detectProfileV2(tmp);
179
+ const rn = results.find(r => r.profile === "react-native");
180
+ const frontend = results.find(r => r.profile === "frontend");
181
+ expect(rn).toBeDefined();
182
+ expect(frontend).toBeDefined();
183
+ expect(rn!.confidence).toBeGreaterThan(frontend!.confidence);
184
+ });
185
+
119
186
  test("results sorted by confidence descending, max 5", () => {
120
187
  writeFileSync(join(tmp, "Cargo.toml"), "");
121
188
  mkdirSync(join(tmp, "src"));
@@ -132,12 +199,109 @@ describe("detectProfileV2", () => {
132
199
  });
133
200
  });
134
201
 
135
- describe("detectProfile (v1 compat)", () => {
136
- test("returns results with confidence 0-100", () => {
137
- writeFileSync(join(tmp, "Cargo.toml"), "");
138
- const results = detectProfile(tmp);
139
- expect(results.length).toBeGreaterThan(0);
140
- expect(results[0]!.confidence).toBeGreaterThan(0);
141
- expect(results[0]!.confidence).toBeLessThanOrEqual(100);
202
+
203
+ describe("detectProfileV2 Python deps", () => {
204
+ test("requirements.txt with boto3 → aws suggested, python outranks it", () => {
205
+ writeFileSync(join(tmp, "requirements.txt"), "boto3==1.34.0\n");
206
+ const results = detectProfileV2(tmp);
207
+ const aws = results.find(r => r.profile === "aws");
208
+ const python = results.find(r => r.profile === "python");
209
+ expect(aws).toBeDefined();
210
+ expect(aws!.confidence).toBeGreaterThanOrEqual(0.5);
211
+ expect(aws!.confidence).toBeLessThan(0.7);
212
+ expect(python).toBeDefined();
213
+ expect(python!.confidence).toBeGreaterThan(aws!.confidence);
214
+ });
215
+
216
+ test("version specifiers, extras, and comments are stripped", () => {
217
+ writeFileSync(join(tmp, "requirements.txt"), [
218
+ "# payments",
219
+ "stripe==7.0.0",
220
+ "psycopg2-binary>=2.9 ; python_version >= '3.8'",
221
+ "uvicorn[standard]~=0.29",
222
+ "",
223
+ ].join("\n"));
224
+ const results = detectProfileV2(tmp);
225
+ expect(results.find(r => r.profile === "stripe")).toBeDefined();
226
+ expect(results.find(r => r.profile === "postgres")).toBeDefined();
227
+ });
228
+
229
+ test("pyproject.toml [project] dependencies → supabase suggested", () => {
230
+ writeFileSync(join(tmp, "pyproject.toml"), [
231
+ "[project]",
232
+ 'name = "myapp"',
233
+ "dependencies = [",
234
+ ' "supabase>=2.0",',
235
+ ' "httpx",',
236
+ "]",
237
+ ].join("\n"));
238
+ const results = detectProfileV2(tmp);
239
+ expect(results.find(r => r.profile === "supabase")).toBeDefined();
240
+ });
241
+
242
+ test("PEP 503 normalization: slack_sdk matches slack-sdk", () => {
243
+ writeFileSync(join(tmp, "requirements.txt"), "slack_sdk==3.27.0\n");
244
+ const results = detectProfileV2(tmp);
245
+ expect(results.find(r => r.profile === "slack")).toBeDefined();
246
+ });
247
+
248
+ test("python files without service deps suggest no service profiles", () => {
249
+ writeFileSync(join(tmp, "requirements.txt"), "requests==2.31.0\nflask\n");
250
+ const results = detectProfileV2(tmp);
251
+ expect(results.find(r => r.profile === "aws")).toBeUndefined();
252
+ expect(results.find(r => r.profile === "stripe")).toBeUndefined();
253
+ });
254
+ });
255
+
256
+ describe("detectProfileV2 — monorepo workspaces", () => {
257
+ test("workspaces glob: packages/*/package.json deps surface at the root", () => {
258
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
259
+ private: true,
260
+ workspaces: ["packages/*"],
261
+ }));
262
+ mkdirSync(join(tmp, "packages/api"), { recursive: true });
263
+ writeFileSync(join(tmp, "packages/api/package.json"), JSON.stringify({
264
+ dependencies: { stripe: "14.0.0" },
265
+ }));
266
+ const results = detectProfileV2(tmp);
267
+ const stripe = results.find(r => r.profile === "stripe");
268
+ expect(stripe).toBeDefined();
269
+ expect(stripe!.reasons.join(" ")).toContain("workspace");
270
+ });
271
+
272
+ test("pnpm-workspace.yaml globs are honored", () => {
273
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({ private: true }));
274
+ writeFileSync(join(tmp, "pnpm-workspace.yaml"), 'packages:\n - "apps/*"\n');
275
+ mkdirSync(join(tmp, "apps/web"), { recursive: true });
276
+ writeFileSync(join(tmp, "apps/web/package.json"), JSON.stringify({
277
+ dependencies: { "@aws-sdk/client-s3": "3.0.0" },
278
+ }));
279
+ const results = detectProfileV2(tmp);
280
+ expect(results.find(r => r.profile === "aws")).toBeDefined();
281
+ });
282
+
283
+ test("exact workspace paths (no glob) are scanned too", () => {
284
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
285
+ private: true,
286
+ workspaces: { packages: ["apps/web"] },
287
+ }));
288
+ mkdirSync(join(tmp, "apps/web"), { recursive: true });
289
+ writeFileSync(join(tmp, "apps/web/package.json"), JSON.stringify({
290
+ dependencies: { "@supabase/supabase-js": "2.0.0" },
291
+ }));
292
+ const results = detectProfileV2(tmp);
293
+ expect(results.find(r => r.profile === "supabase")).toBeDefined();
294
+ });
295
+
296
+ test("packages/ without a workspaces declaration is NOT scanned", () => {
297
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({
298
+ dependencies: { express: "4.0.0" },
299
+ }));
300
+ mkdirSync(join(tmp, "packages/api"), { recursive: true });
301
+ writeFileSync(join(tmp, "packages/api/package.json"), JSON.stringify({
302
+ dependencies: { stripe: "14.0.0" },
303
+ }));
304
+ const results = detectProfileV2(tmp);
305
+ expect(results.find(r => r.profile === "stripe")).toBeUndefined();
142
306
  });
143
307
  });
@@ -3,112 +3,9 @@
3
3
  * Scans cwd for project signals and scores against known profiles.
4
4
  */
5
5
 
6
- import { existsSync, readFileSync } from "node:fs";
6
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
 
9
- interface Signal {
10
- file: string; // glob-like path to check (relative to cwd)
11
- weight: number;
12
- profile: string;
13
- }
14
-
15
- export const SIGNALS: Signal[] = [
16
- // Frontend / Next.js
17
- { file: "next.config.js", weight: 5, profile: "nextjs" },
18
- { file: "next.config.ts", weight: 5, profile: "nextjs" },
19
- { file: "next.config.mjs", weight: 5, profile: "nextjs" },
20
- { file: "app/layout.tsx", weight: 4, profile: "nextjs" },
21
- { file: "app/page.tsx", weight: 3, profile: "nextjs" },
22
- { file: "next.config.js", weight: 4, profile: "frontend" },
23
- { file: "next.config.ts", weight: 4, profile: "frontend" },
24
- { file: "next.config.mjs", weight: 4, profile: "frontend" },
25
- { file: "vite.config.ts", weight: 4, profile: "frontend" },
26
- { file: "vite.config.js", weight: 4, profile: "frontend" },
27
- { file: "tailwind.config.js", weight: 3, profile: "frontend" },
28
- { file: "tailwind.config.ts", weight: 3, profile: "frontend" },
29
- { file: "postcss.config.js", weight: 2, profile: "frontend" },
30
- { file: "tsconfig.json", weight: 1, profile: "frontend" },
31
-
32
- // Backend (Node/TS)
33
- { file: "docker-compose.yml", weight: 3, profile: "backend" },
34
- { file: "docker-compose.yaml", weight: 3, profile: "backend" },
35
- { file: "Dockerfile", weight: 2, profile: "backend" },
36
- { file: "prisma/schema.prisma", weight: 4, profile: "backend" },
37
- { file: "migrations", weight: 3, profile: "backend" },
38
- { file: "drizzle.config.ts", weight: 4, profile: "backend" },
39
- { file: "src/server.ts", weight: 3, profile: "backend" },
40
- { file: "src/index.ts", weight: 1, profile: "backend" },
41
- { file: ".github/workflows/", weight: 1, profile: "backend" },
42
-
43
- // Python API
44
- { file: "pyproject.toml", weight: 4, profile: "python" },
45
- { file: "setup.py", weight: 3, profile: "python" },
46
- { file: "requirements.txt", weight: 3, profile: "python" },
47
- { file: "app/main.py", weight: 5, profile: "python" },
48
- { file: "main.py", weight: 3, profile: "python" },
49
- { file: "manage.py", weight: 5, profile: "python" },
50
- { file: "uvicorn.ini", weight: 4, profile: "python" },
51
- { file: "alembic.ini", weight: 4, profile: "python" },
52
- { file: ".python-version", weight: 2, profile: "python" },
53
-
54
- // Rust
55
- { file: "Cargo.toml", weight: 5, profile: "rust" },
56
- { file: "Cargo.lock", weight: 3, profile: "rust" },
57
- { file: "src/main.rs", weight: 4, profile: "rust" },
58
- { file: "src/lib.rs", weight: 3, profile: "rust" },
59
- { file: ".cargo/config.toml", weight: 2, profile: "rust" },
60
-
61
- // Go API
62
- { file: "go.mod", weight: 5, profile: "go-api" },
63
- { file: "go.sum", weight: 3, profile: "go-api" },
64
- { file: "cmd/", weight: 3, profile: "go-api" },
65
- { file: "internal/", weight: 2, profile: "go-api" },
66
- { file: "main.go", weight: 4, profile: "go-api" },
67
-
68
- // Medusa
69
- { file: "medusa-config.js", weight: 5, profile: "medusa-dev" },
70
- { file: "medusa-config.ts", weight: 5, profile: "medusa-dev" },
71
- { file: "packages/medusa", weight: 5, profile: "medusa-dev" },
72
-
73
- // Docs
74
- { file: "astro.config.mjs", weight: 4, profile: "docs-writer" },
75
- { file: "docusaurus.config.js", weight: 4, profile: "docs-writer" },
76
- { file: "mkdocs.yml", weight: 4, profile: "docs-writer" },
77
- { file: "content/blog", weight: 3, profile: "docs-writer" },
78
- { file: "docs/", weight: 2, profile: "docs-writer" },
79
-
80
- // Fleet
81
- { file: ".colony", weight: 5, profile: "fleet-control" },
82
- { file: ".omx", weight: 4, profile: "fleet-control" },
83
- { file: "scripts/codex-fleet", weight: 5, profile: "fleet-control" },
84
-
85
- // Creative
86
- { file: "design-tokens", weight: 4, profile: "creative-media" },
87
- { file: "figma.config.ts", weight: 4, profile: "creative-media" },
88
-
89
- // Research
90
- { file: "research/", weight: 3, profile: "research" },
91
- { file: "papers/", weight: 3, profile: "research" },
92
-
93
- // Three.js
94
- { file: "three.js", weight: 4, profile: "threejs" },
95
-
96
- // Generic Claude-managed repo → baseline profile
97
- { file: "CLAUDE.md", weight: 2, profile: "core" },
98
- { file: ".claude/", weight: 2, profile: "core" },
99
-
100
- // Full (meta)
101
- { file: "profiles/", weight: 2, profile: "full" },
102
- ];
103
-
104
- export interface DetectionResult {
105
- profile: string;
106
- score: number;
107
- maxScore: number;
108
- confidence: number; // 0-100
109
- signals: string[]; // which files matched
110
- }
111
-
112
9
  /**
113
10
  * V2 detection result with 0-1 confidence and reasons array.
114
11
  */
@@ -145,9 +42,158 @@ function hasAny(deps: Set<string>, names: string[]): boolean {
145
42
  return false;
146
43
  }
147
44
 
45
+ /**
46
+ * Service/integration dependency → profile suggestions. Unlike the framework
47
+ * chain below (mutually exclusive — a repo is *either* a Next.js app or a
48
+ * Vite app), these are additive: a Next.js shop with `stripe` installed gets
49
+ * both `nextjs` and `stripe` suggested. Confidence sits in the
50
+ * [SUGGESTED_MIN_CONFIDENCE, SUGGESTED_AUTO_PICK_CONFIDENCE) band on purpose:
51
+ * high enough to show in the picker, low enough to never outrank the primary
52
+ * stack profile or hijack the Enter default. Only profiles that exist in
53
+ * profiles/ belong here — the picker drops unknown names, but a dead rule is
54
+ * still noise.
55
+ */
56
+ export interface DepProfileRule {
57
+ profile: string;
58
+ /** Exact dependency names that trigger the rule. */
59
+ deps?: string[];
60
+ /** Scoped-package prefixes, e.g. "@aws-sdk/". */
61
+ prefixes?: string[];
62
+ /** Python package names (PEP 503 normalized: lowercase, `_`/`.` → `-`). */
63
+ pyDeps?: string[];
64
+ confidence: number;
65
+ reason: string;
66
+ /**
67
+ * Eligible as a combine-multiselect companion (see `serviceCompanions`).
68
+ * True for service integrations that ride alongside a primary stack;
69
+ * false/omitted for rules that ARE a primary stack (react-native).
70
+ */
71
+ companion?: boolean;
72
+ }
73
+
74
+ export const DEP_PROFILE_RULES: DepProfileRule[] = [
75
+ { profile: "stripe", deps: ["stripe"], prefixes: ["@stripe/"], pyDeps: ["stripe"], confidence: 0.6, reason: "package.json has stripe", companion: true },
76
+ { profile: "aws", deps: ["aws-sdk", "aws-cdk"], prefixes: ["@aws-sdk/", "@aws-cdk/"], pyDeps: ["boto3", "botocore", "aws-cdk-lib"], confidence: 0.6, reason: "package.json has @aws-sdk/*", companion: true },
77
+ { profile: "supabase", prefixes: ["@supabase/"], pyDeps: ["supabase"], confidence: 0.6, reason: "package.json has @supabase/*", companion: true },
78
+ { profile: "slack", prefixes: ["@slack/"], pyDeps: ["slack-sdk"], confidence: 0.6, reason: "package.json has @slack/*", companion: true },
79
+ { profile: "postgres", deps: ["pg", "postgres", "pg-promise"], pyDeps: ["psycopg", "psycopg2", "psycopg2-binary", "asyncpg"], confidence: 0.55, reason: "package.json has pg/postgres", companion: true },
80
+ { profile: "resend", deps: ["resend"], pyDeps: ["resend"], confidence: 0.6, reason: "package.json has resend", companion: true },
81
+ { profile: "strapi", prefixes: ["@strapi/"], confidence: 0.65, reason: "package.json has @strapi/*", companion: true },
82
+ { profile: "threejs", deps: ["three"], confidence: 0.6, reason: "package.json has three", companion: true },
83
+ // react-native is a primary stack, not a service: an RN repo also has
84
+ // `react`, which the framework chain reads as plain `frontend` (0.8) — so
85
+ // this one rule sits above the band to outrank that misread.
86
+ { profile: "react-native", deps: ["react-native", "expo"], prefixes: ["@react-native/"], confidence: 0.85, reason: "package.json has react-native/expo" },
87
+ ];
88
+
148
89
  const ex = (cwd: string, rel: string): boolean => existsSync(join(cwd, rel));
149
90
  const exAny = (cwd: string, rels: string[]): boolean => rels.some((r) => ex(cwd, r));
150
91
 
92
+ /**
93
+ * Cap on workspace child package.jsons read per detection. The picker calls
94
+ * detectProfileV2 synchronously on every launch, so a huge monorepo must not
95
+ * turn profile suggestion into a directory crawl.
96
+ */
97
+ const MAX_WORKSPACE_PKGS = 24;
98
+
99
+ /**
100
+ * Union of dependency names across workspace child packages, so a monorepo
101
+ * ROOT cwd still detects service deps (stripe in packages/api). Patterns come
102
+ * from package.json `workspaces` (array or {packages}) and pnpm-workspace.yaml.
103
+ * Best-effort glob support: exact paths and single trailing `/*` only — deeper
104
+ * globs (`**`, or a `*` mid-path) are skipped, negations ignored.
105
+ */
106
+ function readWorkspaceDeps(cwd: string): Set<string> {
107
+ const patterns: string[] = [];
108
+ try {
109
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
110
+ const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
111
+ if (Array.isArray(ws)) for (const w of ws) if (typeof w === "string") patterns.push(w);
112
+ } catch { /* no root package.json */ }
113
+ try {
114
+ const raw = readFileSync(join(cwd, "pnpm-workspace.yaml"), "utf8");
115
+ const section = raw.match(/^packages:\s*\n((?:[ \t]*-[^\n]*\n?)*)/m);
116
+ if (section) {
117
+ for (const m of section[1]!.matchAll(/-\s*["']?([^"'\n#]+)/g)) patterns.push(m[1]!.trim());
118
+ }
119
+ } catch { /* no pnpm workspace file */ }
120
+
121
+ const deps = new Set<string>();
122
+ let read = 0;
123
+ for (const pattern of patterns) {
124
+ if (pattern.startsWith("!")) continue;
125
+ const dirs: string[] = [];
126
+ if (pattern.endsWith("/*") && !pattern.slice(0, -2).includes("*")) {
127
+ const base = pattern.slice(0, -2);
128
+ try {
129
+ for (const e of readdirSync(join(cwd, base), { withFileTypes: true })) {
130
+ if (e.isDirectory()) dirs.push(join(base, e.name));
131
+ }
132
+ } catch { /* glob base missing */ }
133
+ } else if (!pattern.includes("*")) {
134
+ dirs.push(pattern);
135
+ }
136
+ for (const dir of dirs) {
137
+ if (read >= MAX_WORKSPACE_PKGS) return deps;
138
+ try {
139
+ const pkg = JSON.parse(readFileSync(join(cwd, dir, "package.json"), "utf8"));
140
+ read += 1;
141
+ for (const k of Object.keys(pkg.dependencies ?? {})) deps.add(k);
142
+ for (const k of Object.keys(pkg.devDependencies ?? {})) deps.add(k);
143
+ } catch { /* dir without package.json */ }
144
+ }
145
+ }
146
+ return deps;
147
+ }
148
+
149
+ /** PEP 503 name normalization: lowercase, runs of `-`/`_`/`.` → single `-`. */
150
+ const normPyName = (name: string): string => name.toLowerCase().replace(/[-_.]+/g, "-");
151
+
152
+ /**
153
+ * Best-effort Python dependency names from requirements.txt and
154
+ * pyproject.toml, for the `pyDeps` side of DEP_PROFILE_RULES. requirements
155
+ * lines are parsed properly (comments, extras, version specifiers, env
156
+ * markers stripped); pyproject is a cheap regex over quoted strings in
157
+ * `dependencies = [...]` arrays plus `[tool.poetry.dependencies]` keys — not
158
+ * a TOML parser, same best-effort discipline as the rest of this module.
159
+ * `source` names the file that contributed deps, for detection reasons.
160
+ */
161
+ function readPythonDeps(cwd: string): { deps: Set<string>; source: string } {
162
+ const deps = new Set<string>();
163
+ const sources: string[] = [];
164
+ try {
165
+ const raw = readFileSync(join(cwd, "requirements.txt"), "utf8");
166
+ for (const line of raw.split("\n")) {
167
+ const bare = line.split("#")[0]!.trim();
168
+ if (!bare || bare.startsWith("-")) continue; // blank / pip flags (-r, -e, --hash)
169
+ const m = bare.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/);
170
+ if (m) deps.add(normPyName(m[1]!));
171
+ }
172
+ if (deps.size > 0) sources.push("requirements.txt");
173
+ } catch { /* no requirements.txt */ }
174
+ try {
175
+ const raw = readFileSync(join(cwd, "pyproject.toml"), "utf8");
176
+ const before = deps.size;
177
+ // Quoted entries inside any `dependencies = [...]` array ([project] or
178
+ // optional-dependencies groups).
179
+ for (const arr of raw.matchAll(/dependencies\s*=\s*\[([^\]]*)\]/g)) {
180
+ for (const q of arr[1]!.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)/g)) {
181
+ deps.add(normPyName(q[1]!));
182
+ }
183
+ }
184
+ // `[tool.poetry.dependencies]` table keys (one `name = ...` per line).
185
+ const poetry = raw.match(/\[tool\.poetry\.dependencies\]([^[]*)/);
186
+ if (poetry) {
187
+ for (const line of poetry[1]!.split("\n")) {
188
+ const m = line.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/);
189
+ if (m && m[1] !== "python") deps.add(normPyName(m[1]!));
190
+ }
191
+ }
192
+ if (deps.size > before) sources.push("pyproject.toml");
193
+ } catch { /* no pyproject.toml */ }
194
+ return { deps, source: sources.join(" + ") };
195
+ }
196
+
151
197
  /**
152
198
  * Per-extra-signal confidence boost. A profile backed by several independent
153
199
  * signals (e.g. `medusa-config.ts` + `@medusajs/*` dep) is a stronger match
@@ -208,6 +254,11 @@ export function detectProfileV2(cwd: string): DetectionResultV2[] {
208
254
  if (exAny(cwd, ["vite.config.ts", "vite.config.js"])) add("frontend", 0.6, "vite.config.*");
209
255
  if (exAny(cwd, ["tailwind.config.js", "tailwind.config.ts"])) add("frontend", 0.4, "tailwind.config.*");
210
256
 
257
+ // ── Vercel (deploy target) — an explicit vercel.json / .vercel signals intent
258
+ // to use Vercel, so it edges out the bare framework profile (nextjs/frontend).
259
+ if (exAny(cwd, ["vercel.json", ".vercel/project.json"])) add("vercel", 0.95, "vercel.json");
260
+ else if (ex(cwd, ".vercel")) add("vercel", 0.9, ".vercel/");
261
+
211
262
  // ── Docs ──
212
263
  if (exAny(cwd, ["astro.config.mjs", "docusaurus.config.js", "mkdocs.yml"])) {
213
264
  add("docs-writer", 0.7, "astro / docusaurus / mkdocs config");
@@ -244,6 +295,42 @@ export function detectProfileV2(cwd: string): DetectionResultV2[] {
244
295
  } else {
245
296
  add("backend", 0.6, "package.json (no framework)");
246
297
  }
298
+ // Vercel CLI / SDK in deps corroborates an existing vercel.json signal.
299
+ if (hasPrefix(allDeps, "@vercel/") || allDeps.has("vercel")) {
300
+ add("vercel", 0.6, "package.json @vercel/* or vercel");
301
+ }
302
+ // Service/integration deps (stripe, @aws-sdk/*, …) — additive, see table.
303
+ for (const rule of DEP_PROFILE_RULES) {
304
+ const hit =
305
+ (rule.deps !== undefined && hasAny(allDeps, rule.deps)) ||
306
+ (rule.prefixes ?? []).some((p) => hasPrefix(allDeps, p));
307
+ if (hit) add(rule.profile, rule.confidence, rule.reason);
308
+ }
309
+ }
310
+
311
+ // ── Workspace child deps (monorepo roots) — same rule table ──
312
+ if (exAny(cwd, ["package.json", "pnpm-workspace.yaml"])) {
313
+ const wsDeps = readWorkspaceDeps(cwd);
314
+ if (wsDeps.size > 0) {
315
+ for (const rule of DEP_PROFILE_RULES) {
316
+ const hit =
317
+ (rule.deps !== undefined && hasAny(wsDeps, rule.deps)) ||
318
+ (rule.prefixes ?? []).some((p) => hasPrefix(wsDeps, p));
319
+ if (hit) add(rule.profile, rule.confidence, `workspace ${rule.reason}`);
320
+ }
321
+ }
322
+ }
323
+
324
+ // ── Python deps (requirements.txt / pyproject.toml) — same rule table ──
325
+ if (exAny(cwd, ["requirements.txt", "pyproject.toml"])) {
326
+ const { deps: pyDeps, source } = readPythonDeps(cwd);
327
+ if (pyDeps.size > 0) {
328
+ for (const rule of DEP_PROFILE_RULES) {
329
+ if (rule.pyDeps === undefined) continue;
330
+ const matched = rule.pyDeps.find((d) => pyDeps.has(d));
331
+ if (matched !== undefined) add(rule.profile, rule.confidence, `${source} has ${matched}`);
332
+ }
333
+ }
247
334
  }
248
335
 
249
336
  return [...results.entries()]
@@ -258,35 +345,3 @@ export function detectProfileV2(cwd: string): DetectionResultV2[] {
258
345
  .sort((a, b) => b.confidence - a.confidence)
259
346
  .slice(0, 5);
260
347
  }
261
-
262
- export function detectProfile(cwd: string): DetectionResult[] {
263
- const scores = new Map<string, { score: number; max: number; signals: string[] }>();
264
-
265
- // Compute max possible score per profile
266
- for (const s of SIGNALS) {
267
- const entry = scores.get(s.profile) ?? { score: 0, max: 0, signals: [] };
268
- entry.max += s.weight;
269
- scores.set(s.profile, entry);
270
- }
271
-
272
- // Score based on what exists
273
- for (const s of SIGNALS) {
274
- const target = join(cwd, s.file);
275
- if (existsSync(target)) {
276
- const entry = scores.get(s.profile)!;
277
- entry.score += s.weight;
278
- entry.signals.push(s.file);
279
- }
280
- }
281
-
282
- return [...scores.entries()]
283
- .map(([profile, d]) => ({
284
- profile,
285
- score: d.score,
286
- maxScore: d.max,
287
- confidence: d.max > 0 ? Math.round((d.score / d.max) * 100) : 0,
288
- signals: d.signals,
289
- }))
290
- .filter(r => r.score > 0)
291
- .sort((a, b) => b.confidence - a.confidence || b.score - a.score);
292
- }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Codex routing parity: a profile's persona (e.g. the core "Model routing"
3
+ * block) must reach BOTH agents' memory files — CLAUDE.md for claude-code and
4
+ * AGENTS.md for codex. The persona is the portable layer of the model-routing
5
+ * feature; the model-route-nudge hook is Claude-only (Codex has no
6
+ * UserPromptSubmit equivalent), so this guards the part Codex actually gets.
7
+ *
8
+ * Standalone file (not folded into runtime-materializer.test.ts) to avoid a
9
+ * cross-agent file lock on that test.
10
+ */
11
+
12
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
13
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ import { materializeRuntime } from "./runtime-materializer";
18
+ import type { ResolvedProfile } from "../../profiles/_types";
19
+
20
+ let root: string;
21
+ beforeEach(async () => { root = await mkdtemp(join(tmpdir(), "cue-persona-")); });
22
+ afterEach(async () => { await rm(root, { recursive: true, force: true }); });
23
+
24
+ const MARKER = "## Model routing — route by task hardness";
25
+
26
+ const profile: ResolvedProfile = {
27
+ name: "persona-parity-test",
28
+ description: "test",
29
+ agents: ["claude-code", "codex"],
30
+ skills: { local: [], npx: [] },
31
+ mcps: [],
32
+ plugins: [],
33
+ env: {},
34
+ inheritanceChain: ["persona-parity-test"],
35
+ // persona is read via (profile as any).persona by the materializer.
36
+ persona: `${MARKER}\n delegate EASY/SEARCH work to a Sonnet subagent`,
37
+ } as ResolvedProfile;
38
+
39
+ const common = {
40
+ runtimeRoot: "",
41
+ skillSourceLookup: async (id: string) => `/fake/skills/${id}`,
42
+ mcpRegistry: {},
43
+ userClaudeMd: "",
44
+ };
45
+
46
+ describe("persona reaches both agents' memory files", () => {
47
+ test("claude-code → CLAUDE.md contains the persona", async () => {
48
+ const out = await materializeRuntime({ ...common, runtimeRoot: join(root, "rt"), profile, agent: "claude-code" });
49
+ const md = await readFile(join(out.runtimeDir, "CLAUDE.md"), "utf8");
50
+ expect(md).toContain(MARKER);
51
+ });
52
+
53
+ test("codex → AGENTS.md contains the persona (routing parity)", async () => {
54
+ const out = await materializeRuntime({ ...common, runtimeRoot: join(root, "rt"), profile, agent: "codex" });
55
+ const md = await readFile(join(out.runtimeDir, "AGENTS.md"), "utf8");
56
+ expect(md).toContain(MARKER);
57
+ });
58
+ });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { detectCompanions, type CompanionDetectInput } from "./companion-detect";
3
+ import { detectCompanions, serviceCompanions, type CompanionDetectInput } from "./companion-detect";
4
+ import { COMBINE_AUTO_CHECK_CONFIDENCE } from "./picker";
4
5
 
5
6
  const KNOWN = new Set(["higgsfield", "blog-writer", "postizz", "creative-media"]);
6
7
 
@@ -106,3 +107,44 @@ describe("detectCompanions — gating & robustness", () => {
106
107
  expect(detectCompanions({ cwd: "/work/x", knownProfiles: KNOWN })).toBeInstanceOf(Array);
107
108
  });
108
109
  });
110
+
111
+ describe("serviceCompanions", () => {
112
+ const SERVICE_KNOWN = new Set(["stripe", "aws", "react-native", "nextjs"]);
113
+
114
+ test("dep-detected stripe becomes a pre-checked combine companion", () => {
115
+ const out = serviceCompanions(
116
+ [{ profile: "stripe", confidence: 0.6, reasons: ["package.json has stripe"] }],
117
+ SERVICE_KNOWN,
118
+ );
119
+ expect(out).toHaveLength(1);
120
+ expect(out[0]!.profile).toBe("stripe");
121
+ // At/above the combine multiselect's auto-check line (0.7) so the row
122
+ // starts checked — a direct dependency is a strong companion signal.
123
+ expect(out[0]!.confidence).toBeGreaterThanOrEqual(COMBINE_AUTO_CHECK_CONFIDENCE);
124
+ expect(out[0]!.reason).toBe("package.json has stripe");
125
+ });
126
+
127
+ test("primary-stack rules (react-native) never become companions", () => {
128
+ const out = serviceCompanions(
129
+ [{ profile: "react-native", confidence: 0.85, reasons: ["package.json has react-native/expo"] }],
130
+ SERVICE_KNOWN,
131
+ );
132
+ expect(out).toEqual([]);
133
+ });
134
+
135
+ test("non-rule detections (nextjs from framework chain) pass through nothing", () => {
136
+ const out = serviceCompanions(
137
+ [{ profile: "nextjs", confidence: 0.9, reasons: ["package.json has next"] }],
138
+ SERVICE_KNOWN,
139
+ );
140
+ expect(out).toEqual([]);
141
+ });
142
+
143
+ test("profiles not installed in this cue install are filtered out", () => {
144
+ const out = serviceCompanions(
145
+ [{ profile: "aws", confidence: 0.6, reasons: ["package.json has @aws-sdk/*"] }],
146
+ new Set(["stripe"]),
147
+ );
148
+ expect(out).toEqual([]);
149
+ });
150
+ });