cue-ai 0.9.2 → 0.9.4

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 +154 -394
  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
@@ -38,18 +38,23 @@ function cue(args: string[], opts: { cwd?: string; env?: Record<string, string>
38
38
 
39
39
  describe.skipIf(!BUN_SPAWNABLE)("cue launch e2e", () => {
40
40
  let tmpDir: string;
41
+ let oldXdgConfigHome: string | undefined;
41
42
 
42
43
  beforeEach(async () => {
43
44
  tmpDir = await mkdtemp(join(tmpdir(), "cue-e2e-launch-"));
45
+ oldXdgConfigHome = process.env.XDG_CONFIG_HOME;
46
+ process.env.XDG_CONFIG_HOME = join(tmpDir, "xdg");
44
47
  });
45
48
 
46
49
  afterEach(async () => {
50
+ if (oldXdgConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
51
+ else process.env.XDG_CONFIG_HOME = oldXdgConfigHome;
47
52
  await rm(tmpDir, { recursive: true, force: true });
48
53
  });
49
54
 
50
- test("launch --rematerialize with .cue-profile resolves and builds runtime", async () => {
51
- // Create a .cue-profile pointing to a real profile
52
- await writeFile(join(tmpDir, ".cue-profile"), "caveman-quick\n");
55
+ test("launch --rematerialize with .cue.profile resolves and builds runtime", async () => {
56
+ // Create a .cue.profile pointing to a real profile
57
+ await writeFile(join(tmpDir, ".cue.profile"), "caveman-quick\n");
53
58
 
54
59
  const res = cue(["launch", "claude", "--rematerialize"], { cwd: tmpDir });
55
60
 
@@ -67,7 +72,7 @@ describe.skipIf(!BUN_SPAWNABLE)("cue launch e2e", () => {
67
72
  });
68
73
 
69
74
  test("launch --rematerialize second call is cache hit (rebuilt=false)", async () => {
70
- await writeFile(join(tmpDir, ".cue-profile"), "core\n");
75
+ await writeFile(join(tmpDir, ".cue.profile"), "core\n");
71
76
 
72
77
  const first = cue(["launch", "claude", "--rematerialize"], { cwd: tmpDir });
73
78
  expect(first.status).toBe(0);
@@ -83,12 +88,12 @@ describe.skipIf(!BUN_SPAWNABLE)("cue launch e2e", () => {
83
88
  expect(secondJson.profile).toBe("core");
84
89
  });
85
90
 
86
- test("launch resolves profile from .cue-profile in parent directory", async () => {
87
- // Create a subdirectory and put .cue-profile in parent
91
+ test("launch resolves profile from .cue.profile in parent directory", async () => {
92
+ // Create a subdirectory and put .cue.profile in parent
88
93
  const { mkdir } = await import("node:fs/promises");
89
94
  const subDir = join(tmpDir, "src", "lib");
90
95
  await mkdir(subDir, { recursive: true });
91
- await writeFile(join(tmpDir, ".cue-profile"), "rust\n");
96
+ await writeFile(join(tmpDir, ".cue.profile"), "rust\n");
92
97
 
93
98
  const res = cue(["launch", "claude", "--rematerialize"], { cwd: subDir });
94
99
  expect(res.status).toBe(0);
@@ -97,7 +102,7 @@ describe.skipIf(!BUN_SPAWNABLE)("cue launch e2e", () => {
97
102
  });
98
103
 
99
104
  test("launch produces CLAUDE.md with profile stamp in runtime dir", async () => {
100
- await writeFile(join(tmpDir, ".cue-profile"), "backend\n");
105
+ await writeFile(join(tmpDir, ".cue.profile"), "backend\n");
101
106
 
102
107
  const res = cue(["launch", "claude", "--rematerialize"], { cwd: tmpDir });
103
108
  expect(res.status).toBe(0);
@@ -109,7 +114,7 @@ describe.skipIf(!BUN_SPAWNABLE)("cue launch e2e", () => {
109
114
  });
110
115
 
111
116
  test("launch produces settings.json with MCPs and plugins", async () => {
112
- await writeFile(join(tmpDir, ".cue-profile"), "backend\n");
117
+ await writeFile(join(tmpDir, ".cue.profile"), "backend\n");
113
118
 
114
119
  const res = cue(["launch", "claude", "--rematerialize"], { cwd: tmpDir });
115
120
  expect(res.status).toBe(0);
@@ -121,7 +126,7 @@ describe.skipIf(!BUN_SPAWNABLE)("cue launch e2e", () => {
121
126
  });
122
127
 
123
128
  test("launch creates skills/ symlinks in runtime dir", async () => {
124
- await writeFile(join(tmpDir, ".cue-profile"), "backend\n");
129
+ await writeFile(join(tmpDir, ".cue.profile"), "backend\n");
125
130
 
126
131
  const res = cue(["launch", "claude", "--rematerialize"], { cwd: tmpDir });
127
132
  expect(res.status).toBe(0);
@@ -29,12 +29,13 @@ import {
29
29
  import { loadProfile, listProfiles, listFeaturedProfiles, parseProfileSelector } from "../lib/profile-loader";
30
30
  import { resolveProfileForCwd } from "../lib/cwd-resolver";
31
31
  import { DIVIDER_PREFIX, runPicker, type PickerOption, type ProfileTally } from "../lib/picker";
32
- import { materializeRuntime, type McpServerConfig } from "../lib/runtime-materializer";
33
- import { resolveLocalSkill, listAllSkillIds } from "../lib/resolver-local";
32
+ import { materializeRuntime } from "../lib/runtime-materializer";
33
+ import { resolveLocalSkill } from "../lib/resolver-local";
34
+ import { expandSkillWildcards, loadMcpRegistry, resolveClaudeCredentialsSource as resolveSharedClaudeCredentialsSource } from "../lib/runtime-install";
34
35
  import { detectKittyTerminal, kittyPlaceholderLabel, transmitKittyImage } from "../lib/kitty-image";
35
36
  import { computeStats } from "../lib/analytics";
36
37
  import { detectProfileV2, type DetectionResultV2 } from "../lib/auto-detect";
37
- import { detectCompanions, type CompanionSignal } from "../lib/companion-detect";
38
+ import { detectCompanions, serviceCompanions, type CompanionSignal } from "../lib/companion-detect";
38
39
  import type { ResolvedProfile } from "../../profiles/_types";
39
40
  import type { ProfileAffinity, UniversalSuggestion } from "../lib/pair-suggestions";
40
41
  import { hasWorkspaces, getActiveWorkspace, computeOverrides, resolveWorkspaceForCwd } from "../lib/workspaces";
@@ -144,6 +145,18 @@ function execAgent(bin: string, args: string[], env: NodeJS.ProcessEnv): Promise
144
145
  });
145
146
  }
146
147
 
148
+ function isAgentHelpPassthrough(parsed: ParsedArgs): boolean {
149
+ return (
150
+ !parsed.override &&
151
+ !parsed.forcePick &&
152
+ !parsed.dryRun &&
153
+ !parsed.rematerialize &&
154
+ parsed.subset === null &&
155
+ parsed.passthrough.length === 1 &&
156
+ (parsed.passthrough[0] === "--help" || parsed.passthrough[0] === "-h")
157
+ );
158
+ }
159
+
147
160
  export interface TmuxAnnounceExtras {
148
161
  /** Token-overhead summary: dot = 🟢/🟡/🟠/🔴, size = "8K". Both optional. */
149
162
  overhead?: { dot: string; size: string };
@@ -281,16 +294,7 @@ function announceTmuxProfile(
281
294
  * Used by both the launch hot path and the picker `details` callback so the
282
295
  * shown summary matches what materializeRuntime will actually link.
283
296
  */
284
- async function expandWildcards(profile: ResolvedProfile): Promise<void> {
285
- if (!profile.skills.local.some((s) => s.id === "*/*")) return;
286
- const allIds = await listAllSkillIds();
287
- const wildcard = profile.skills.local.find((s) => s.id === "*/*")!;
288
- const existing = new Set(profile.skills.local.filter((s) => s.id !== "*/*").map((s) => s.id));
289
- profile.skills.local = [
290
- ...profile.skills.local.filter((s) => s.id !== "*/*"),
291
- ...allIds.filter((id) => !existing.has(id)).map((id) => ({ ...wildcard, id })),
292
- ];
293
- }
297
+ const expandWildcards = expandSkillWildcards;
294
298
 
295
299
  /**
296
300
  * Compact human-readable summary of what a profile would load. Each returned
@@ -1061,52 +1065,6 @@ async function listProfileOptions(pinnedProfile?: string): Promise<PickerOption[
1061
1065
  return buildPickerSections(defaultOpt, sorted, recent, 3, Date.now(), suggested, featured);
1062
1066
  }
1063
1067
 
1064
- async function loadMcpRegistry(agent: "claude-code" | "codex"): Promise<Record<string, McpServerConfig>> {
1065
- const root = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(
1066
- new URL(import.meta.url).pathname,
1067
- "..",
1068
- "..",
1069
- "..",
1070
- );
1071
- // Files to merge, in priority order. The master `claude.sanitized.json` wins
1072
- // on key collisions; `claude_runtime.sanitized.json` is the live snapshot
1073
- // captured from the user's actual `~/.claude.json` (covers servers
1074
- // registered at runtime but not yet promoted to the master registry).
1075
- // Without this merge, profiles like `marketing` that reference
1076
- // `reddit`/`google-ads-mcp`/`meta-ads`/`Higgsfield` (runtime-only entries)
1077
- // would silently drop those MCPs at materialize time.
1078
- const files = agent === "claude-code"
1079
- ? ["claude_runtime.sanitized.json", "claude.sanitized.json"]
1080
- : ["codex.sanitized.json"];
1081
-
1082
- const merged: Record<string, McpServerConfig> = {};
1083
- for (const file of files) {
1084
- const path = join(root, "resources", "mcps", "configs", file);
1085
- try {
1086
- const text = await readFile(path, "utf8");
1087
- const raw = JSON.parse(text) as { servers?: Record<string, McpServerConfig> };
1088
- for (const [k, v] of Object.entries(raw.servers ?? {})) {
1089
- // First file wins (claude_runtime first, then claude master).
1090
- // We want master to win, so only set if not already present.
1091
- if (!(k in merged)) merged[k] = v;
1092
- }
1093
- } catch { /* file missing — skip */ }
1094
- }
1095
- // Second pass: let the master registry override the runtime snapshot
1096
- // (master is the curated source of truth; runtime is just a fallback).
1097
- const masterPath = join(root, "resources", "mcps", "configs",
1098
- agent === "claude-code" ? "claude.sanitized.json" : "codex.sanitized.json");
1099
- try {
1100
- const text = await readFile(masterPath, "utf8");
1101
- const raw = JSON.parse(text) as { servers?: Record<string, McpServerConfig> };
1102
- for (const [k, v] of Object.entries(raw.servers ?? {})) {
1103
- merged[k] = v;
1104
- }
1105
- } catch (err) { debug("launch:master-config", err); /* keep runtime fallbacks */ }
1106
-
1107
- return merged;
1108
- }
1109
-
1110
1068
  async function readSharedClaudeMd(profile?: { name: string; inheritanceChain?: string[] }): Promise<string> {
1111
1069
  const root = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(
1112
1070
  new URL(import.meta.url).pathname, "..", "..", "..",
@@ -1147,15 +1105,17 @@ async function readSharedClaudeMd(profile?: { name: string; inheritanceChain?: s
1147
1105
  async function buildUserClaudeMd(profile: ResolvedProfile, agent: "claude-code" | "codex"): Promise<string> {
1148
1106
  let content = await readSharedClaudeMd(profile) + await readUserClaudeMd(agent);
1149
1107
 
1150
- // First-time profile suggestion: if no .cue-profile in cwd, inject marker
1151
- const cueProfilePath = join(process.cwd(), ".cue-profile");
1108
+ // First-time profile suggestion: if no .cue.profile in cwd, inject marker
1109
+ const cueProfilePath = join(process.cwd(), ".cue.profile");
1152
1110
  if (!existsSync(cueProfilePath)) {
1153
1111
  content += "\n<!-- cue:first-time-suggest -->\n" +
1154
1112
  "## ⚡ First-Time Setup\n\n" +
1155
- "No `.cue-profile` is pinned to this directory. Before answering the user's first message, " +
1156
- "quickly scan this repo (package.json, Cargo.toml, Dockerfile, etc.) and suggest the best " +
1157
- "profile from `cue list`. Present your suggestion in 3-4 lines with reasoning, then offer " +
1158
- "to pin it with `echo <name> > .cue-profile`. After suggesting, proceed with the user's request.\n\n" +
1113
+ "No `.cue.profile` is pinned to this directory. Before answering the user's first message, " +
1114
+ "summon the right profile into THIS session — no restart. Invoke the `meta/profile-summon` " +
1115
+ "skill, or run `cue summon` (auto-detects from the repo). It soft-loads the profile's persona " +
1116
+ "and skill playbooks inline, pins `.cue.profile`, and prints `claude --continue` for the MCP / " +
1117
+ "/slash-command tail (which needs a warm re-exec). Propose the detected profile in 3-4 lines, " +
1118
+ "apply on the user's OK, then proceed with their request.\n\n" +
1159
1119
  "Available profiles:\n```\n" +
1160
1120
  (await getProfileListForStamp()) +
1161
1121
  "```\n\n";
@@ -1238,56 +1198,31 @@ async function findRealBinary(name: string): Promise<string | null> {
1238
1198
  * accountUuid and copies the freshest one back to source.
1239
1199
  */
1240
1200
  async function resolveClaudeCredentialsSource(): Promise<string> {
1241
- const picked = await pickClaudeCredentialsSource();
1242
- // Heal source from freshest sibling runtime (if any). Silent best-effort.
1243
- try {
1244
- const { syncFreshestToSource } = await import("../lib/credentials-sync");
1245
- const runtimeRoot = join(configDir(), "runtime");
1246
- const result = await syncFreshestToSource(picked, runtimeRoot);
1247
- if (result.synced) {
1248
- // Tiny breadcrumb so users can see when the heal kicked in. Stays on
1249
- // stderr so it doesn't pollute pipelines or `claude --print` output.
1250
- process.stderr.write(
1251
- `▸ cue: refreshed source credentials from a sibling runtime (rotated refresh-token healed)\n`,
1252
- );
1253
- }
1254
- } catch (err) { debug("launch:runtime-heal", err); /* best-effort — never blocks launch */ }
1255
- return picked;
1201
+ return resolveSharedClaudeCredentialsSource({ healFromRuntime: true });
1256
1202
  }
1257
1203
 
1258
- async function pickClaudeCredentialsSource(): Promise<string> {
1259
- if (process.env.CLAUDE_CONFIG_DIR) return process.env.CLAUDE_CONFIG_DIR;
1260
-
1261
- const homeClaude = join(homedir(), ".claude");
1262
- if (existsSync(join(homeClaude, ".credentials.json"))) return homeClaude;
1263
-
1204
+ /**
1205
+ * Write the runtime's login-fresh `.credentials.json` back to the account
1206
+ * dir that owns it (matched by accountUuid). Runs (a) before materialization
1207
+ * so the account-identity guard can't destroy the only live copy of the
1208
+ * OTHER account's rotated token when two authmux accounts alternate on one
1209
+ * profile — and (b) after the agent exits, so a `/login` done inside the
1210
+ * session lands in the account's CLAUDE_CONFIG_DIR immediately instead of
1211
+ * waiting for that account's next launch. Best-effort: never blocks launch.
1212
+ */
1213
+ async function rescueRuntimeCredsToOwner(profileName: string): Promise<void> {
1264
1214
  try {
1265
- const { spawnSync } = await import("node:child_process");
1266
- const { statSync } = await import("node:fs");
1267
- const res = spawnSync("authmux", ["parallel", "--list", "--json"], {
1268
- encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "pipe"],
1269
- });
1270
- if (res.status === 0 && res.stdout) {
1271
- const parsed = JSON.parse(res.stdout) as { data?: { profiles?: Array<{ name: string; configDir: string }> } };
1272
- const profiles = parsed?.data?.profiles ?? [];
1273
- const withMtime = profiles
1274
- .map((p) => {
1275
- const credsPath = join(p.configDir, ".credentials.json");
1276
- let mtime = 0;
1277
- try { mtime = statSync(credsPath).mtimeMs; } catch { /* missing */ }
1278
- return { ...p, mtime };
1279
- })
1280
- .filter((p) => p.mtime > 0)
1281
- .sort((a, b) => b.mtime - a.mtime);
1282
- const pick = withMtime[0];
1283
- if (pick) {
1284
- process.stderr.write(`▸ cue: inheriting auth from authmux profile "${pick.name}"\n`);
1285
- return pick.configDir;
1286
- }
1215
+ const { listKnownAccountDirs, rescueRuntimeCredentials } = await import("../lib/credentials-sync");
1216
+ // basename() pins the path inside the runtime tree — this helper WRITES
1217
+ // token files, so a profile name with a path separator must not escape.
1218
+ const runtimeClaudeDir = join(configDir(), "runtime", basename(profileName), "claude");
1219
+ const result = await rescueRuntimeCredentials(runtimeClaudeDir, await listKnownAccountDirs(homedir()));
1220
+ if (result.rescued) {
1221
+ process.stderr.write(`▸ cue: wrote login-fresh credentials back to ${result.to}\n`);
1287
1222
  }
1288
- } catch { /* authmux not installed or query failed — fall through */ }
1289
-
1290
- return homeClaude;
1223
+ } catch (err) {
1224
+ debug("launch:cred-rescue", err);
1225
+ }
1291
1226
  }
1292
1227
 
1293
1228
  // ---------------------------------------------------------------------------
@@ -1308,6 +1243,16 @@ export async function run(args: string[]): Promise<number> {
1308
1243
  process.stderr.write("cue launch: missing agent (use 'claude' or 'codex')\n");
1309
1244
  return 1;
1310
1245
  }
1246
+ if (isAgentHelpPassthrough(parsed)) {
1247
+ const realBin = await findRealBinary(parsed.agent);
1248
+ if (!realBin) {
1249
+ process.stderr.write(
1250
+ `cue launch: couldn't find the real '${parsed.agent}' binary on PATH=${process.env.PATH}\n`,
1251
+ );
1252
+ return 127;
1253
+ }
1254
+ return execAgent(realBin, parsed.passthrough, process.env);
1255
+ }
1311
1256
  const agentKind = parsed.agent === "claude" ? "claude-code" : "codex";
1312
1257
 
1313
1258
  // Resolve profile.
@@ -1405,16 +1350,19 @@ export async function run(args: string[]): Promise<number> {
1405
1350
  // with what the directory actually looks like (e.g. picking medusa-next
1406
1351
  // in a vite.config.ts project).
1407
1352
  let detected: ReadonlyArray<{ name: string; reasons: string[]; confidence: number }> = [];
1353
+ let rawDetections: DetectionResultV2[] = [];
1408
1354
  try {
1409
- detected = detectProfileV2(cwd)
1410
- .filter((d) => knownProfileNames.has(d.profile))
1411
- .map((d) => ({ name: d.profile, reasons: d.reasons, confidence: d.confidence }));
1355
+ rawDetections = detectProfileV2(cwd).filter((d) => knownProfileNames.has(d.profile));
1356
+ detected = rawDetections.map((d) => ({ name: d.profile, reasons: d.reasons, confidence: d.confidence }));
1412
1357
  } catch (err) { debug("launch:autodetect", err); }
1413
1358
  // Content-aware combine companions: scan the cwd for asset/draft/brand
1414
- // signals and feed matching profiles into the combine multiselect.
1359
+ // signals and feed matching profiles into the combine multiselect — plus
1360
+ // dep-detected service profiles (stripe, @aws-sdk/*, …), which join as
1361
+ // pre-checked rows (see serviceCompanions).
1415
1362
  let companions: CompanionSignal[] = [];
1416
1363
  try {
1417
1364
  companions = detectCompanions({ cwd, knownProfiles: knownProfileNames, brands: listPostizzBrands() });
1365
+ companions = companions.concat(serviceCompanions(rawDetections, knownProfileNames));
1418
1366
  } catch (err) { debug("launch:companions", err); }
1419
1367
  // Cross-profile combine suggestions offered under every primary: the curated
1420
1368
  // `_featured.yaml` set (improver, secops, builder, …) plus the profiles the
@@ -1635,6 +1583,11 @@ export async function run(args: string[]): Promise<number> {
1635
1583
  }
1636
1584
  }
1637
1585
 
1586
+ // Rescue-before-wipe: if this runtime's credentials belong to a different
1587
+ // account than credentialsSource, the materializer's identity guard is
1588
+ // about to discard them — return them to their owning account dir first.
1589
+ if (agentKind === "claude-code") await rescueRuntimeCredsToOwner(profileName);
1590
+
1638
1591
  const runtime = await materializeRuntime({
1639
1592
  profile: await applyWorkspaceOverrides(profile),
1640
1593
  agent: agentKind,
@@ -1918,5 +1871,9 @@ export async function run(args: string[]): Promise<number> {
1918
1871
  health: healthBadge,
1919
1872
  });
1920
1873
 
1921
- return execAgent(realBin, parsed.passthrough, childEnv);
1874
+ const exitCode = await execAgent(realBin, parsed.passthrough, childEnv);
1875
+ // Persist any /login done inside the session to its account dir now —
1876
+ // don't leave the only live rotated token stranded in the shared runtime.
1877
+ if (agentKind === "claude-code") await rescueRuntimeCredsToOwner(profileName);
1878
+ return exitCode;
1922
1879
  }
@@ -50,7 +50,7 @@ Agents: ${AGENT_IDS.join(", ")}
50
50
 
51
51
  Flags:
52
52
  --dir <path> Target directory (default: cwd or agent's config dir)
53
- --profile <name> Use specific profile (default: resolved from .cue-profile)
53
+ --profile <name> Use specific profile (default: resolved from .cue.profile)
54
54
  --all Materialize for all agents listed in the profile
55
55
  --dry-run Show what would be written without writing
56
56
 
@@ -89,7 +89,7 @@ Examples:
89
89
  if (!name) throw new Error("no active profile");
90
90
  profile = await loadProfile(name);
91
91
  } catch {
92
- process.stderr.write("No active profile. Pin one with `echo <name> > .cue-profile`\n");
92
+ process.stderr.write("No active profile. Pin one with `echo <name> > .cue.profile`\n");
93
93
  return 1;
94
94
  }
95
95
 
@@ -77,7 +77,7 @@ function helpText(): string {
77
77
 
78
78
  function resolveActiveProfile(explicit: string | null): string | null {
79
79
  if (explicit) return explicit;
80
- const pin = join(process.cwd(), ".cue-profile");
80
+ const pin = join(process.cwd(), ".cue.profile");
81
81
  if (existsSync(pin)) {
82
82
  try {
83
83
  const txt = readFileSync(pin, "utf8").trim().split("\n")[0]?.trim();
@@ -126,7 +126,7 @@ export async function runSecurityAudit(args: string[]): Promise<number> {
126
126
  }
127
127
 
128
128
  if (!profileName) {
129
- process.stderr.write("No active profile. Specify a profile name or set .cue-profile.\n");
129
+ process.stderr.write("No active profile. Specify a profile name or set .cue.profile.\n");
130
130
  return 1;
131
131
  }
132
132
 
@@ -3,7 +3,7 @@
3
3
  * `cue shell install` — install shims
4
4
  *
5
5
  * Usage: eval "$(cue shell hook)"
6
- * Adds a cd wrapper that checks .cue-profile on directory change.
6
+ * Adds a cd wrapper that checks .cue.profile on directory change.
7
7
  */
8
8
 
9
9
  import { existsSync, readFileSync, statSync, accessSync, constants } from "node:fs";
@@ -33,8 +33,8 @@ __cue_check_profile() {
33
33
  local dir="$PWD"
34
34
  local profile=""
35
35
  while [ "$dir" != "/" ] && [ "$dir" != "$HOME" ]; do
36
- if [ -f "$dir/.cue-profile" ]; then
37
- profile="$(cat "$dir/.cue-profile" 2>/dev/null | tr -d '\\n')"
36
+ if [ -f "$dir/.cue.profile" ]; then
37
+ profile="$(cat "$dir/.cue.profile" 2>/dev/null | tr -d '\\n')"
38
38
  break
39
39
  fi
40
40
  dir="$(dirname "$dir")"
@@ -57,8 +57,8 @@ __cue_check_profile() {
57
57
  local dir="$PWD"
58
58
  local profile=""
59
59
  while [[ "$dir" != "/" && "$dir" != "$HOME" ]]; do
60
- if [[ -f "$dir/.cue-profile" ]]; then
61
- profile="$(cat "$dir/.cue-profile" | tr -d '\\n')"
60
+ if [[ -f "$dir/.cue.profile" ]]; then
61
+ profile="$(cat "$dir/.cue.profile" | tr -d '\\n')"
62
62
  break
63
63
  fi
64
64
  dir="$(dirname "$dir")"
@@ -82,8 +82,8 @@ function __cue_check_profile --on-variable PWD
82
82
  set -l dir $PWD
83
83
  set -l profile ""
84
84
  while test "$dir" != "/" -a "$dir" != "$HOME"
85
- if test -f "$dir/.cue-profile"
86
- set profile (cat "$dir/.cue-profile" | string trim)
85
+ if test -f "$dir/.cue.profile"
86
+ set profile (cat "$dir/.cue.profile" | string trim)
87
87
  break
88
88
  end
89
89
  set dir (dirname "$dir")
@@ -74,7 +74,7 @@ function helpText(): string {
74
74
 
75
75
  function resolveActiveProfile(explicit: string | null): string | null {
76
76
  if (explicit) return explicit;
77
- const pin = join(process.cwd(), ".cue-profile");
77
+ const pin = join(process.cwd(), ".cue.profile");
78
78
  if (existsSync(pin)) {
79
79
  try {
80
80
  const txt = readFileSync(pin, "utf8").trim().split("\n")[0]?.trim();
@@ -219,7 +219,7 @@ async function cmdSearch(query: string, json: boolean): Promise<number> {
219
219
  async function cmdAddToProfile(id: string, preview = false): Promise<number> {
220
220
  const profileName = await getActiveProfileName();
221
221
  if (!profileName) {
222
- process.stderr.write("No active profile. Pin one with `echo <name> > .cue-profile`\n");
222
+ process.stderr.write("No active profile. Pin one with `echo <name> > .cue.profile`\n");
223
223
  return 1;
224
224
  }
225
225
 
@@ -808,8 +808,8 @@ async function cmdNpxAdd(args: string[]): Promise<number> {
808
808
  // #2: Auto-pin option
809
809
  const pin = await p.confirm({ message: `Pin "${name}" to current directory?`, initialValue: true });
810
810
  if (!p.isCancel(pin) && pin) {
811
- await writeFile(join(process.cwd(), ".cue-profile"), `${name}\n`);
812
- p.log.success(`Pinned → .cue-profile`);
811
+ await writeFile(join(process.cwd(), ".cue.profile"), `${name}\n`);
812
+ p.log.success(`Pinned → .cue.profile`);
813
813
  }
814
814
 
815
815
  // #10: Post-create launch prompt
@@ -25,7 +25,7 @@ async function cmdSnapshot(args: string[]): Promise<number> {
25
25
 
26
26
  const profileName = await resolveActiveProfile();
27
27
  if (!profileName) {
28
- process.stderr.write("No active profile. Pin one with `echo <name> > .cue-profile`\n");
28
+ process.stderr.write("No active profile. Pin one with `echo <name> > .cue.profile`\n");
29
29
  return 1;
30
30
  }
31
31
 
@@ -101,6 +101,6 @@ async function cmdRestore(args: string[]): Promise<number> {
101
101
 
102
102
  writeFileSync(join(profileDir, "profile.yaml"), yaml.stringify(profileYaml));
103
103
  process.stdout.write(`✅ Restored profile "${snapshot.profile.name}" from snapshot\n`);
104
- process.stdout.write(` Pin with: echo ${snapshot.profile.name} > .cue-profile\n`);
104
+ process.stdout.write(` Pin with: echo ${snapshot.profile.name} > .cue.profile\n`);
105
105
  return 0;
106
106
  }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
2
+ import { mkdtemp, rm, writeFile, readFile, stat } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { summon, detectActiveProfile, REEXEC_CMD } from "./summon";
7
+
8
+ let dir: string;
9
+ beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), "cue-summon-")); });
10
+ afterEach(async () => { await rm(dir, { recursive: true, force: true }); });
11
+
12
+ describe("detectActiveProfile", () => {
13
+ test("prefers CUE_ACTIVE_PROFILE, then CUE_PROFILE", () => {
14
+ // Arrange / Act / Assert
15
+ expect(detectActiveProfile({ CUE_ACTIVE_PROFILE: "a", CUE_PROFILE: "b" } as NodeJS.ProcessEnv)).toBe("a");
16
+ expect(detectActiveProfile({ CUE_PROFILE: "b" } as NodeJS.ProcessEnv)).toBe("b");
17
+ });
18
+
19
+ test("falls back to the CLAUDE_CONFIG_DIR runtime path", () => {
20
+ const env = { CLAUDE_CONFIG_DIR: "/home/u/.config/cue/runtime/core+skill-writer/claude" } as NodeJS.ProcessEnv;
21
+ expect(detectActiveProfile(env)).toBe("core+skill-writer");
22
+ });
23
+
24
+ test("returns null when nothing identifies the session", () => {
25
+ expect(detectActiveProfile({} as NodeJS.ProcessEnv)).toBeNull();
26
+ });
27
+ });
28
+
29
+ describe("summon", () => {
30
+ test("explicit profile arg overrides an auto-detect signal", async () => {
31
+ // Arrange: a dir that WOULD auto-detect vercel...
32
+ await writeFile(join(dir, "vercel.json"), "{}");
33
+ // Act: ...but an explicit profile is passed.
34
+ const r = await summon({ cwd: dir, profile: "core", active: null, noPin: true });
35
+ // Assert
36
+ expect(r.profile).toBe("core");
37
+ expect(r.detected).toBe(false);
38
+ expect(r.reexec_cmd).toBe(REEXEC_CMD);
39
+ expect(r.skills.length).toBeGreaterThan(0);
40
+ expect(r.skills.every((s) => s.id.length > 0 && typeof s.mcp_status === "string")).toBe(true);
41
+ });
42
+
43
+ test("auto-detects vercel from vercel.json + @vercel deps", async () => {
44
+ // Arrange
45
+ await writeFile(join(dir, "vercel.json"), "{}");
46
+ await writeFile(join(dir, "next.config.js"), "");
47
+ await writeFile(join(dir, "package.json"), JSON.stringify({ dependencies: { next: "15", vercel: "39" } }));
48
+ // Act
49
+ const r = await summon({ cwd: dir, active: null, noPin: true });
50
+ // Assert
51
+ expect(r.profile).toBe("vercel");
52
+ expect(r.detected).toBe(true);
53
+ expect(r.confidence ?? 0).toBeGreaterThanOrEqual(0.9);
54
+ expect(r.persona.length).toBeGreaterThan(0);
55
+ });
56
+
57
+ test("throws when no profile resolves from an empty dir", async () => {
58
+ await expect(summon({ cwd: dir, active: null, noPin: true })).rejects.toThrow();
59
+ });
60
+
61
+ test("throws on an unknown explicit profile", async () => {
62
+ await expect(summon({ cwd: dir, profile: "does-not-exist-xyz", active: null })).rejects.toThrow(/unknown profile/);
63
+ });
64
+
65
+ test("writes the .cue.profile pin by default, skips it with noPin", async () => {
66
+ // Arrange / Act
67
+ const r1 = await summon({ cwd: dir, profile: "vercel", active: null });
68
+ // Assert
69
+ expect(r1.pin_written).toBe(true);
70
+ expect(r1.pin_path).toBe(join(dir, ".cue.profile"));
71
+ expect((await readFile(join(dir, ".cue.profile"), "utf8")).trim()).toBe("vercel");
72
+
73
+ const r2 = await summon({ cwd: dir, profile: "vercel", active: null, noPin: true });
74
+ expect(r2.pin_written).toBe(false);
75
+ });
76
+
77
+ test("dry-run computes a result without writing the pin", async () => {
78
+ const r = await summon({ cwd: dir, profile: "vercel", active: null, dryRun: true });
79
+ expect(r.pin_written).toBe(false);
80
+ expect(r.pin_previous).toBeNull();
81
+ await expect(stat(join(dir, ".cue.profile"))).rejects.toThrow();
82
+ });
83
+
84
+ test("re-pinning the same profile is a no-op, not a clobber", async () => {
85
+ // Arrange: already pinned to vercel
86
+ await writeFile(join(dir, ".cue.profile"), "vercel\n");
87
+ // Act
88
+ const r = await summon({ cwd: dir, profile: "vercel", active: null });
89
+ // Assert: no rewrite, but the prior pin is surfaced
90
+ expect(r.pin_written).toBe(false);
91
+ expect(r.pin_previous).toBe("vercel");
92
+ });
93
+
94
+ test("re-pinning a different profile surfaces the replaced pin", async () => {
95
+ // Arrange: pinned to a different profile
96
+ await writeFile(join(dir, ".cue.profile"), "core\n");
97
+ // Act
98
+ const r = await summon({ cwd: dir, profile: "vercel", active: null });
99
+ // Assert: written, and the previous pin is reported (not silently clobbered)
100
+ expect(r.pin_written).toBe(true);
101
+ expect(r.pin_previous).toBe("core");
102
+ expect((await readFile(join(dir, ".cue.profile"), "utf8")).trim()).toBe("vercel");
103
+ });
104
+
105
+ test("mcp_status reflects the active session's loaded MCPs", async () => {
106
+ // browser/lightpanda needs the `lightpanda` MCP; core loads it.
107
+ const lp = (skills: { id: string; mcp_status: string }[]) =>
108
+ skills.find((s) => s.id === "browser/lightpanda");
109
+
110
+ const noActive = await summon({ cwd: dir, profile: "vercel", active: null, noPin: true });
111
+ const withCore = await summon({ cwd: dir, profile: "vercel", active: "core", noPin: true });
112
+
113
+ expect(lp(noActive.skills)?.mcp_status).toBe("missing:lightpanda");
114
+ expect(lp(withCore.skills)?.mcp_status).toBe("ok");
115
+ });
116
+ });