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.
- package/CHANGELOG.md +4 -3
- package/README.md +154 -394
- package/bin/cue-learnings +30 -4
- package/bin/cue-review-progress +0 -0
- package/bin/cue-review-watch +0 -0
- package/dist/cue.js +4328 -3108
- package/package.json +1 -1
- package/plugins/cue/commands/cue-switch.md +1 -1
- package/plugins/cue/commands/cue.md +1 -1
- package/profiles/backend/profile.yaml +4 -0
- package/profiles/browser/profile.yaml +4 -0
- package/profiles/career/profile.yaml +2 -13
- package/profiles/commerce/profile.yaml +0 -2
- package/profiles/coolify/profile.yaml +0 -1
- package/profiles/core/profile.yaml +78 -11
- package/profiles/dash-merge-test/profile.yaml +6 -1
- package/profiles/designer/profile.yaml +9 -1
- package/profiles/dropshipping/profile.yaml +69 -0
- package/profiles/frontend/profile.yaml +4 -0
- package/profiles/google-ads/profile.yaml +34 -0
- package/profiles/google-analytics/profile.yaml +34 -0
- package/profiles/google-drive/profile.yaml +34 -0
- package/profiles/gstack/profile.yaml +117 -29
- package/profiles/marketing/profile.yaml +0 -1
- package/profiles/media/README.md +70 -0
- package/profiles/media/profile.yaml +104 -0
- package/profiles/nano-banana/profile.yaml +52 -0
- package/profiles/ops/profile.yaml +1 -2
- package/profiles/secops/profile.yaml +3 -0
- package/profiles/skill-writer/profile.yaml +15 -0
- package/profiles/video/profile.yaml +3 -0
- package/profiles/web-frontend-base/profile.yaml +6 -0
- package/profiles/webshop/profile.yaml +0 -1
- package/profiles/webshop-google/profile.yaml +1 -0
- package/profiles/x-growth-bot/profile.yaml +2 -0
- package/resources/icons/generate-icons.py +2 -128
- package/resources/mcps/configs/claude.sanitized.json +88 -20
- package/resources/mcps/configs/claude_runtime.sanitized.json +40 -1
- package/resources/mcps/configs/codex.sanitized.json +29 -0
- package/resources/skills/skills/career/job-hunter/LICENSE +21 -0
- package/resources/skills/skills/career/job-hunter/README.md +323 -0
- package/resources/skills/skills/career/job-hunter/SKILL.md +91 -0
- package/resources/skills/skills/career/job-hunter/agents/README.md +96 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-assessment-prep.md +195 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-ats-scan.md +155 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-bias-audit.md +224 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-cover-letter.md +69 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-decode-jd.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-fit-score.md +183 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-audit.md +74 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-scrape.md +255 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-portfolio-brief.md +123 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-reality-check.md +164 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-reference-prep.md +150 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-rejection-analysis.md +172 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-resume.md +70 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-skills-gap-filler.md +109 -0
- package/resources/skills/skills/career/job-hunter/agents/career-internal.md +94 -0
- package/resources/skills/skills/career/job-hunter/agents/career-linkedin-content.md +173 -0
- package/resources/skills/skills/career/job-hunter/agents/career-linkedin-scanner.md +262 -0
- package/resources/skills/skills/career/job-hunter/agents/career-network-message.md +108 -0
- package/resources/skills/skills/career/job-hunter/agents/career-promote.md +102 -0
- package/resources/skills/skills/career/job-hunter/agents/career-review.md +71 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-debrief.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-mock.md +171 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-panel-decoder.md +152 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-prep.md +184 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-question-bank.md +133 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-research.md +148 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-compare.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-counteroffer.md +144 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-deadline-manager.md +148 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-negotiate.md +126 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-schedule.md +99 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-thankyou.md +80 -0
- package/resources/skills/skills/career/job-hunter/agents/search-company-research.md +146 -0
- package/resources/skills/skills/career/job-hunter/agents/search-follow-up.md +129 -0
- package/resources/skills/skills/career/job-hunter/agents/search-ghost-job-detector.md +152 -0
- package/resources/skills/skills/career/job-hunter/agents/search-inbox-scan.md +193 -0
- package/resources/skills/skills/career/job-hunter/agents/search-interview-scorecard.md +164 -0
- package/resources/skills/skills/career/job-hunter/agents/search-jobs.md +149 -0
- package/resources/skills/skills/career/job-hunter/agents/search-momentum-check.md +194 -0
- package/resources/skills/skills/career/job-hunter/agents/search-outreach.md +85 -0
- package/resources/skills/skills/career/job-hunter/agents/search-referral-finder.md +124 -0
- package/resources/skills/skills/career/job-hunter/agents/search-salary.md +96 -0
- package/resources/skills/skills/career/job-hunter/agents/search-send-email.md +109 -0
- package/resources/skills/skills/career/job-hunter/agents/search-tracker-update.md +127 -0
- package/resources/skills/skills/career/job-hunter/inputs/README.md +26 -0
- package/resources/skills/skills/career/job-hunter/inputs/apply-linkedin-url.txt +8 -0
- package/resources/skills/skills/career/job-hunter/inputs/interview-context.md +24 -0
- package/resources/skills/skills/career/job-hunter/inputs/job-description.md +20 -0
- package/resources/skills/skills/career/job-hunter/inputs/job-search-criteria.md +36 -0
- package/resources/skills/skills/career/job-hunter/inputs/my-linkedin.md +24 -0
- package/resources/skills/skills/career/job-hunter/inputs/my-resume.md +28 -0
- package/resources/skills/skills/career/job-hunter/inputs/search-outreach-target.md +24 -0
- package/resources/skills/skills/career/job-hunter/rules/README.md +37 -0
- package/resources/skills/skills/career/job-hunter/rules/writing-rules.md +81 -0
- package/resources/skills/skills/design/banana/SKILL.md +375 -0
- package/resources/skills/skills/design/banana/references/cost-tracking.md +47 -0
- package/resources/skills/skills/design/banana/references/gemini-models.md +236 -0
- package/resources/skills/skills/design/banana/references/mcp-tools.md +145 -0
- package/resources/skills/skills/design/banana/references/post-processing.md +192 -0
- package/resources/skills/skills/design/banana/references/presets.md +69 -0
- package/resources/skills/skills/design/banana/references/prompt-engineering.md +481 -0
- package/resources/skills/skills/design/banana/scripts/batch.py +97 -0
- package/resources/skills/skills/design/banana/scripts/cost_tracker.py +191 -0
- package/resources/skills/skills/design/banana/scripts/edit.py +159 -0
- package/resources/skills/skills/design/banana/scripts/generate.py +168 -0
- package/resources/skills/skills/design/banana/scripts/presets.py +154 -0
- package/resources/skills/skills/design/banana/scripts/setup_mcp.py +151 -0
- package/resources/skills/skills/design/banana/scripts/validate_setup.py +133 -0
- package/resources/skills/skills/gstack/ship/SKILL.md +13 -0
- package/resources/skills/skills/media/3d-logo-animation/SKILL.md +59 -0
- package/resources/skills/skills/media/action-figure-generator/SKILL.md +48 -0
- package/resources/skills/skills/media/ad-creative/SKILL.md +79 -0
- package/resources/skills/skills/media/ai-clipping/SKILL.md +194 -0
- package/resources/skills/skills/media/ai-clipping/scripts/run-ai-clipping.sh +200 -0
- package/resources/skills/skills/media/ai-fight-scene/SKILL.md +132 -0
- package/resources/skills/skills/media/amazon-product-listing/SKILL.md +68 -0
- package/resources/skills/skills/media/animal-video-generator/SKILL.md +59 -0
- package/resources/skills/skills/media/award-ceremony-video/SKILL.md +87 -0
- package/resources/skills/skills/media/blog-header/SKILL.md +61 -0
- package/resources/skills/skills/media/brand-kit/SKILL.md +72 -0
- package/resources/skills/skills/media/brochures/SKILL.md +65 -0
- package/resources/skills/skills/media/cartoon-dance-animation/SKILL.md +62 -0
- package/resources/skills/skills/media/character-story-video/SKILL.md +84 -0
- package/resources/skills/skills/media/chibi-collage-effect/SKILL.md +63 -0
- package/resources/skills/skills/media/cinema-director/SKILL.md +93 -0
- package/resources/skills/skills/media/cinema-director/scripts/generate-film.sh +78 -0
- package/resources/skills/skills/media/color-analysis-board/SKILL.md +71 -0
- package/resources/skills/skills/media/core-edit/SKILL.md +48 -0
- package/resources/skills/skills/media/core-edit/edit-image.sh +54 -0
- package/resources/skills/skills/media/core-edit/enhance-image.sh +191 -0
- package/resources/skills/skills/media/core-edit/lipsync.sh +144 -0
- package/resources/skills/skills/media/core-edit/video-effects.sh +193 -0
- package/resources/skills/skills/media/core-media/SKILL.md +49 -0
- package/resources/skills/skills/media/core-media/create-music.sh +169 -0
- package/resources/skills/skills/media/core-media/generate-image.sh +161 -0
- package/resources/skills/skills/media/core-media/generate-video.sh +137 -0
- package/resources/skills/skills/media/core-media/image-to-video.sh +228 -0
- package/resources/skills/skills/media/core-media/schema_data.json +18708 -0
- package/resources/skills/skills/media/core-media/upload.sh +41 -0
- package/resources/skills/skills/media/core-platform/SKILL.md +41 -0
- package/resources/skills/skills/media/core-platform/check-result.sh +37 -0
- package/resources/skills/skills/media/core-platform/setup.sh +31 -0
- package/resources/skills/skills/media/couple-grid-creator/SKILL.md +47 -0
- package/resources/skills/skills/media/design-guide/SKILL.md +73 -0
- package/resources/skills/skills/media/drone-style-video/SKILL.md +61 -0
- package/resources/skills/skills/media/fashion-try-on/SKILL.md +61 -0
- package/resources/skills/skills/media/floor-plan-rendering/SKILL.md +56 -0
- package/resources/skills/skills/media/freeze-effect-video/SKILL.md +100 -0
- package/resources/skills/skills/media/giant-product-showcase/SKILL.md +61 -0
- package/resources/skills/skills/media/instagram-post/SKILL.md +58 -0
- package/resources/skills/skills/media/interior-design/SKILL.md +61 -0
- package/resources/skills/skills/media/interior-design-visualizer/SKILL.md +57 -0
- package/resources/skills/skills/media/jewelry-product-video/SKILL.md +61 -0
- package/resources/skills/skills/media/kdenlive/SKILL.md +106 -0
- package/resources/skills/skills/media/kdenlive/scripts/assemble.sh +57 -0
- package/resources/skills/skills/media/kdenlive/scripts/common.sh +30 -0
- package/resources/skills/skills/media/kdenlive/scripts/inspect.sh +19 -0
- package/resources/skills/skills/media/kdenlive/scripts/reframe.sh +22 -0
- package/resources/skills/skills/media/kdenlive/scripts/render.sh +16 -0
- package/resources/skills/skills/media/kdenlive/scripts/title-card.sh +25 -0
- package/resources/skills/skills/media/keyboard-art-maker/SKILL.md +44 -0
- package/resources/skills/skills/media/logo-branding/SKILL.md +70 -0
- package/resources/skills/skills/media/logo-creator/SKILL.md +80 -0
- package/resources/skills/skills/media/logo-creator/scripts/create-logo.sh +38 -0
- package/resources/skills/skills/media/logo-generator/SKILL.md +56 -0
- package/resources/skills/skills/media/multi-angle-reshoot/SKILL.md +70 -0
- package/resources/skills/skills/media/multi-angle-shots/SKILL.md +73 -0
- package/resources/skills/skills/media/music-video/SKILL.md +61 -0
- package/resources/skills/skills/media/nano-banana/SKILL.md +80 -0
- package/resources/skills/skills/media/nano-banana/scripts/generate-nano-art.sh +54 -0
- package/resources/skills/skills/media/one-shot-video/SKILL.md +56 -0
- package/resources/skills/skills/media/photo-pack-generator/SKILL.md +205 -0
- package/resources/skills/skills/media/photo-pack-generator/scripts/generate-pack.sh +241 -0
- package/resources/skills/skills/media/product-ad-cinematic/SKILL.md +78 -0
- package/resources/skills/skills/media/product-campaign/SKILL.md +76 -0
- package/resources/skills/skills/media/product-showcase-video/SKILL.md +60 -0
- package/resources/skills/skills/media/product-video-ad-maker/SKILL.md +59 -0
- package/resources/skills/skills/media/rednote-cover/SKILL.md +57 -0
- package/resources/skills/skills/media/seedance-2/SKILL.md +632 -0
- package/resources/skills/skills/media/seedance-2/scripts/generate-seedance.sh +701 -0
- package/resources/skills/skills/media/selfie-with-celebrities/SKILL.md +64 -0
- package/resources/skills/skills/media/social-media-video/SKILL.md +277 -0
- package/resources/skills/skills/media/social-media-video/scripts/run-social-video.sh +316 -0
- package/resources/skills/skills/media/social-pack/SKILL.md +58 -0
- package/resources/skills/skills/media/storyboard/SKILL.md +57 -0
- package/resources/skills/skills/media/storyboard-to-cooking-video/SKILL.md +143 -0
- package/resources/skills/skills/media/talking-baby-video/SKILL.md +57 -0
- package/resources/skills/skills/media/ugc-ads-workflow/SKILL.md +70 -0
- package/resources/skills/skills/media/ugc-lifestyle-try-on/SKILL.md +65 -0
- package/resources/skills/skills/media/ugc-video-factory/SKILL.md +134 -0
- package/resources/skills/skills/media/ui-design/SKILL.md +81 -0
- package/resources/skills/skills/media/ui-design/scripts/generate-mockup.sh +49 -0
- package/resources/skills/skills/media/url-to-design/SKILL.md +61 -0
- package/resources/skills/skills/media/workflow/SKILL.md +197 -0
- package/resources/skills/skills/media/workflow/scripts/discover-workflow.sh +18 -0
- package/resources/skills/skills/media/workflow/scripts/generate-workflow.sh +33 -0
- package/resources/skills/skills/media/workflow/scripts/interactive-run.sh +16 -0
- package/resources/skills/skills/media/workflow/scripts/list-workflows.sh +20 -0
- package/resources/skills/skills/media/workflow/scripts/run-workflow.sh +34 -0
- package/resources/skills/skills/media/youtube-shorts/SKILL.md +173 -0
- package/resources/skills/skills/media/youtube-shorts/scripts/run-youtube-shorts.sh +141 -0
- package/resources/skills/skills/media/youtube-thumbnail/SKILL.md +66 -0
- package/resources/skills/skills/meta/cue-developer/references/architecture.md +2 -2
- package/resources/skills/skills/meta/cue-usage/SKILL.md +1 -1
- package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +2 -2
- package/resources/skills/skills/meta/profile-optimizer/SKILL.md +1 -1
- package/resources/skills/skills/meta/profile-suggest/SKILL.md +7 -7
- package/resources/skills/skills/meta/profile-summon/SKILL.md +159 -0
- package/resources/skills/skills/meta/profile-summon/evals/evals.json +53 -0
- package/resources/skills/skills/meta/save-profile/SKILL.md +1 -1
- package/resources/skills/skills/meta/skill-reviewer/SKILL.md +3 -0
- package/resources/skills/skills/meta/skill-reviewer/references/tdd-for-skills.md +55 -0
- package/resources/skills/skills/research/find-skills/SKILL.md +1 -1
- package/resources/skills/skills/review/code-review-deep/SKILL.md +20 -0
- package/resources/skills/skills/security/trivy-scan/SKILL.md +139 -0
- package/resources/skills/skills/security/trivy-scan/scripts/ensure-trivy.sh +21 -0
- package/resources/skills/skills/tools/ccusage/SKILL.md +142 -0
- package/src/commands/_index.ts +8 -0
- package/src/commands/ai.ts +2 -2
- package/src/commands/auto-detect.test.ts +74 -0
- package/src/commands/auto-detect.ts +9 -7
- package/src/commands/cli.test.ts +20 -4
- package/src/commands/cli.ts +36 -20
- package/src/commands/create-profile.ts +2 -2
- package/src/commands/debug.ts +2 -2
- package/src/commands/discover.ts +14 -4
- package/src/commands/export-docker.ts +1 -1
- package/src/commands/features-batch1.test.ts +1 -1
- package/src/commands/gates.ts +1 -1
- package/src/commands/import-profile.ts +1 -1
- package/src/commands/init.ts +15 -11
- package/src/commands/install.test.ts +192 -0
- package/src/commands/install.ts +610 -0
- package/src/commands/launch-handoff.e2e.test.ts +33 -1
- package/src/commands/launch.e2e.test.ts +15 -10
- package/src/commands/launch.ts +73 -116
- package/src/commands/materialize.ts +2 -2
- package/src/commands/prune.ts +1 -1
- package/src/commands/security-audit.ts +1 -1
- package/src/commands/shell.ts +7 -7
- package/src/commands/skill-report.ts +1 -1
- package/src/commands/skills.ts +3 -3
- package/src/commands/snapshot.ts +2 -2
- package/src/commands/summon.test.ts +116 -0
- package/src/commands/summon.ts +338 -0
- package/src/commands/trigger-gaps.ts +1 -1
- package/src/commands/use.ts +47 -3
- package/src/commands/watch-live.ts +5 -5
- package/src/commands/watch.ts +8 -8
- package/src/index.ts +2 -0
- package/src/lib/active-sessions.test.ts +3 -3
- package/src/lib/active-sessions.ts +4 -4
- package/src/lib/auto-detect.test.ts +172 -8
- package/src/lib/auto-detect.ts +191 -136
- package/src/lib/codex-persona-parity.test.ts +58 -0
- package/src/lib/companion-detect.test.ts +43 -1
- package/src/lib/companion-detect.ts +35 -0
- package/src/lib/credentials-sync.test.ts +121 -1
- package/src/lib/credentials-sync.ts +95 -1
- package/src/lib/cwd-resolver.test.ts +8 -8
- package/src/lib/cwd-resolver.ts +2 -2
- package/src/lib/dashboard-merge.test.ts +9 -4
- package/src/lib/dashboard-server.ts +1 -1
- package/src/lib/picker.test.ts +1 -1
- package/src/lib/picker.ts +5 -5
- package/src/lib/profile-merge.test.ts +8 -0
- package/src/lib/profile-names.test.ts +3 -3
- package/src/lib/runtime-install.ts +166 -0
- package/src/lib/runtime-materializer.test.ts +137 -0
- package/src/lib/runtime-materializer.ts +105 -2
- package/src/lib/skill-router.test.ts +38 -0
- package/src/lib/skill-router.ts +65 -4
- package/profiles/eu-tender-research/README.md +0 -48
- package/profiles/eu-tender-research/logo.png +0 -0
- package/profiles/eu-tender-research/profile.yaml +0 -108
|
@@ -53,6 +53,29 @@ describe("materializeRuntime", () => {
|
|
|
53
53
|
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test("surfaces allowlisted profile.env (CLAUDE_CODE_SUBAGENT_MODEL) into settings.env", async () => {
|
|
57
|
+
const out = await materializeRuntime({
|
|
58
|
+
profile: {
|
|
59
|
+
...sampleProfile,
|
|
60
|
+
env: {
|
|
61
|
+
CLAUDE_CODE_SUBAGENT_MODEL: "claude-sonnet-4-6",
|
|
62
|
+
// secret reference — must NOT leak into settings.json
|
|
63
|
+
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
agent: "claude-code",
|
|
67
|
+
runtimeRoot: join(root, "runtime"),
|
|
68
|
+
skillSourceLookup: async (id) => `/fake/skills/${id}`,
|
|
69
|
+
mcpRegistry: { "claude-mem": { command: "claude-mem", args: [] } },
|
|
70
|
+
userClaudeMd: "# user CLAUDE.md\n",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const settings = JSON.parse(await readFile(join(out.runtimeDir, "settings.json"), "utf8"));
|
|
74
|
+
expect(settings.env).toEqual({ CLAUDE_CODE_SUBAGENT_MODEL: "claude-sonnet-4-6" });
|
|
75
|
+
// unresolved "${...}" placeholder and non-allowlisted keys stay out
|
|
76
|
+
expect(settings.env.AWS_SECRET_ACCESS_KEY).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
56
79
|
test("second call with same profile is a no-op (rebuilt=false)", async () => {
|
|
57
80
|
const args = {
|
|
58
81
|
profile: sampleProfile,
|
|
@@ -396,6 +419,120 @@ describe("materializeRuntime", () => {
|
|
|
396
419
|
expect(creds.claudeAiOauth.refreshToken).toBe("live");
|
|
397
420
|
});
|
|
398
421
|
|
|
422
|
+
test("credentialsSource: cache hit re-seeds a FILE .claude.json on account switch", async () => {
|
|
423
|
+
// Regression: runtime dirs are keyed by profile, so two authmux accounts
|
|
424
|
+
// share one runtime. Claude's atomic rewrite turns the .claude.json symlink
|
|
425
|
+
// into a local FILE owned by the last-logged-in account; the overlay's
|
|
426
|
+
// "cue override — don't touch" rule then left the OLD account's identity
|
|
427
|
+
// paired with the NEW account's tokens, forcing a re-login every time the
|
|
428
|
+
// accounts alternated on a profile.
|
|
429
|
+
const credSrcA = join(root, "accA");
|
|
430
|
+
const credSrcB = join(root, "accB");
|
|
431
|
+
await mkdir(credSrcA, { recursive: true });
|
|
432
|
+
await mkdir(credSrcB, { recursive: true });
|
|
433
|
+
await writeFile(join(credSrcA, ".credentials.json"), '{"claudeAiOauth":{"refreshToken":"A"}}');
|
|
434
|
+
await writeFile(join(credSrcB, ".credentials.json"), '{"claudeAiOauth":{"refreshToken":"B"}}');
|
|
435
|
+
await writeFile(join(credSrcA, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "uuid-A" } }));
|
|
436
|
+
await writeFile(join(credSrcB, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "uuid-B" } }));
|
|
437
|
+
|
|
438
|
+
const args = {
|
|
439
|
+
profile: { ...sampleProfile, name: "acct-switch" },
|
|
440
|
+
agent: "claude-code" as const,
|
|
441
|
+
runtimeRoot: join(root, "runtime"),
|
|
442
|
+
skillSourceLookup: async (id: string) => `/fake/source/${id}`,
|
|
443
|
+
mcpRegistry: { "claude-mem": { command: "claude-mem" } },
|
|
444
|
+
userClaudeMd: "",
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const first = await materializeRuntime({ ...args, credentialsSource: credSrcA });
|
|
448
|
+
expect(first.rebuilt).toBe(true);
|
|
449
|
+
// Simulate Claude Code's atomic rewrite: the .claude.json symlink becomes
|
|
450
|
+
// a local regular file carrying account A's identity + session state.
|
|
451
|
+
await rm(join(first.runtimeDir, ".claude.json"), { force: true });
|
|
452
|
+
await writeFile(
|
|
453
|
+
join(first.runtimeDir, ".claude.json"),
|
|
454
|
+
JSON.stringify({ oauthAccount: { accountUuid: "uuid-A" }, projects: { "/w": {} } }),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Account B launches the same profile → cache hit → identity must follow.
|
|
458
|
+
const second = await materializeRuntime({ ...args, credentialsSource: credSrcB });
|
|
459
|
+
expect(second.rebuilt).toBe(false);
|
|
460
|
+
const cj = JSON.parse(await readFile(join(second.runtimeDir, ".claude.json"), "utf8"));
|
|
461
|
+
expect(cj.oauthAccount.accountUuid).toBe("uuid-B");
|
|
462
|
+
const creds = JSON.parse(await readFile(join(second.runtimeDir, ".credentials.json"), "utf8"));
|
|
463
|
+
expect(creds.claudeAiOauth.refreshToken).toBe("B");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("credentialsSource: cache hit keeps a FILE .claude.json when the account matches", async () => {
|
|
467
|
+
// Same-account relaunch must NOT clobber per-profile session state
|
|
468
|
+
// (projects list etc.) that Claude wrote into the runtime's local file.
|
|
469
|
+
const credSrc = join(root, "accSame");
|
|
470
|
+
await mkdir(credSrc, { recursive: true });
|
|
471
|
+
await writeFile(join(credSrc, ".credentials.json"), '{"claudeAiOauth":{"refreshToken":"A"}}');
|
|
472
|
+
await writeFile(join(credSrc, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "uuid-A" } }));
|
|
473
|
+
|
|
474
|
+
const args = {
|
|
475
|
+
profile: { ...sampleProfile, name: "acct-same" },
|
|
476
|
+
agent: "claude-code" as const,
|
|
477
|
+
runtimeRoot: join(root, "runtime"),
|
|
478
|
+
skillSourceLookup: async (id: string) => `/fake/source/${id}`,
|
|
479
|
+
mcpRegistry: { "claude-mem": { command: "claude-mem" } },
|
|
480
|
+
userClaudeMd: "",
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const first = await materializeRuntime({ ...args, credentialsSource: credSrc });
|
|
484
|
+
await rm(join(first.runtimeDir, ".claude.json"), { force: true });
|
|
485
|
+
const localState = JSON.stringify({ oauthAccount: { accountUuid: "uuid-A" }, projects: { "/w": { history: [1] } } });
|
|
486
|
+
await writeFile(join(first.runtimeDir, ".claude.json"), localState);
|
|
487
|
+
|
|
488
|
+
const second = await materializeRuntime({ ...args, credentialsSource: credSrc });
|
|
489
|
+
expect(second.rebuilt).toBe(false);
|
|
490
|
+
// Identity + per-profile session state preserved (syncMcpsIntoClaudeJson
|
|
491
|
+
// legitimately rewrites the file to merge mcpServers, so compare fields,
|
|
492
|
+
// not bytes).
|
|
493
|
+
const cj = JSON.parse(await readFile(join(second.runtimeDir, ".claude.json"), "utf8"));
|
|
494
|
+
expect(cj.oauthAccount.accountUuid).toBe("uuid-A");
|
|
495
|
+
expect(cj.projects).toEqual({ "/w": { history: [1] } });
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("credentialsSource: rebuild does not resurrect another account's identity or tokens", async () => {
|
|
499
|
+
// The preserve step's expiresAt comparison is meaningless across accounts:
|
|
500
|
+
// the old runtime's token may expire later yet belong to the OTHER account.
|
|
501
|
+
// On a cross-account rebuild the source state must win wholesale.
|
|
502
|
+
const credSrcA = join(root, "rebA");
|
|
503
|
+
const credSrcB = join(root, "rebB");
|
|
504
|
+
await mkdir(credSrcA, { recursive: true });
|
|
505
|
+
await mkdir(credSrcB, { recursive: true });
|
|
506
|
+
const LATER = 9_000_000_000_000;
|
|
507
|
+
await writeFile(join(credSrcA, ".credentials.json"), JSON.stringify({ claudeAiOauth: { expiresAt: LATER, refreshToken: "A" } }));
|
|
508
|
+
await writeFile(join(credSrcB, ".credentials.json"), JSON.stringify({ claudeAiOauth: { expiresAt: 1_000, refreshToken: "B" } }));
|
|
509
|
+
await writeFile(join(credSrcA, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "uuid-A" } }));
|
|
510
|
+
await writeFile(join(credSrcB, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "uuid-B" } }));
|
|
511
|
+
|
|
512
|
+
const base = {
|
|
513
|
+
agent: "claude-code" as const,
|
|
514
|
+
runtimeRoot: join(root, "runtime"),
|
|
515
|
+
skillSourceLookup: async (id: string) => `/fake/source/${id}`,
|
|
516
|
+
mcpRegistry: { "claude-mem": { command: "claude-mem" } },
|
|
517
|
+
userClaudeMd: "",
|
|
518
|
+
};
|
|
519
|
+
const p1: ResolvedProfile = { ...sampleProfile, name: "acct-rebuild" };
|
|
520
|
+
const p2: ResolvedProfile = { ...sampleProfile, name: "acct-rebuild", skills: { local: [{ id: "design/ui-ux-pro-max" }, { id: "design/extra" }], npx: [] } };
|
|
521
|
+
|
|
522
|
+
const first = await materializeRuntime({ ...base, profile: p1, credentialsSource: credSrcA });
|
|
523
|
+
// Claude's rewrite pins account A's identity into the runtime as a FILE.
|
|
524
|
+
await rm(join(first.runtimeDir, ".claude.json"), { force: true });
|
|
525
|
+
await writeFile(join(first.runtimeDir, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "uuid-A" } }));
|
|
526
|
+
|
|
527
|
+
// Account B relaunches with a changed profile → rebuild + preserve step.
|
|
528
|
+
const second = await materializeRuntime({ ...base, profile: p2, credentialsSource: credSrcB });
|
|
529
|
+
expect(second.rebuilt).toBe(true);
|
|
530
|
+
const creds = JSON.parse(await readFile(join(second.runtimeDir, ".credentials.json"), "utf8"));
|
|
531
|
+
expect(creds.claudeAiOauth.refreshToken).toBe("B");
|
|
532
|
+
const cj = JSON.parse(await readFile(join(second.runtimeDir, ".claude.json"), "utf8"));
|
|
533
|
+
expect(cj.oauthAccount.accountUuid).toBe("uuid-B");
|
|
534
|
+
});
|
|
535
|
+
|
|
399
536
|
test("CLAUDE.md stamp uses real ISO timestamp, not literal $(date)", async () => {
|
|
400
537
|
const out = await materializeRuntime({
|
|
401
538
|
profile: sampleProfile,
|
|
@@ -531,11 +531,18 @@ export async function materializeRuntime(input: MaterializeInput): Promise<Mater
|
|
|
531
531
|
process.env.CUE_TRIGGER_PHRASES === "1" ||
|
|
532
532
|
process.env.CUE_TRIGGER_PHRASES === "true"
|
|
533
533
|
);
|
|
534
|
+
// Cap the capability table so heavy profiles (60+ skills) don't blow past
|
|
535
|
+
// Claude Code's 40KB CLAUDE.md perf threshold. Overflow skills stay listed
|
|
536
|
+
// under "Available Skills" and loadable on demand. Override with
|
|
537
|
+
// CUE_MAX_CAPABILITY_ROWS (0 disables the cap).
|
|
538
|
+
const maxCapEnv = Number(process.env.CUE_MAX_CAPABILITY_ROWS);
|
|
539
|
+
const maxCapabilityRows = Number.isFinite(maxCapEnv) && maxCapEnv >= 0 ? maxCapEnv : 50;
|
|
534
540
|
const routerBlock = renderRouter(routerParsed, {
|
|
535
541
|
overrides: routerOverrides,
|
|
536
542
|
zombies: zombieIds,
|
|
537
543
|
lean,
|
|
538
544
|
omitTriggerPhrases,
|
|
545
|
+
maxCapabilityRows,
|
|
539
546
|
});
|
|
540
547
|
if (routerBlock) stamp += routerBlock;
|
|
541
548
|
|
|
@@ -716,7 +723,21 @@ export async function materializeRuntime(input: MaterializeInput): Promise<Mater
|
|
|
716
723
|
// rm ~/.config/cue/runtime/<profile>/claude/.credentials.json
|
|
717
724
|
// rm ~/.config/cue/runtime/<profile>/claude/.claude.json
|
|
718
725
|
// Next launch will copy current source state.
|
|
719
|
-
|
|
726
|
+
// Account-identity guard: runtime dirs are keyed by PROFILE, so two authmux
|
|
727
|
+
// accounts (claude-account1 / claude-account2 with different
|
|
728
|
+
// CLAUDE_CONFIG_DIRs) share the same runtime. When the OLD runtime belongs
|
|
729
|
+
// to a different account than the current credentialsSource, resurrecting
|
|
730
|
+
// its .claude.json/.credentials.json would pair the old account's identity
|
|
731
|
+
// with the new account's tokens (or vice versa) — and the expiresAt
|
|
732
|
+
// comparison below is meaningless across accounts. Skip preservation
|
|
733
|
+
// entirely and let the overlay's source state win.
|
|
734
|
+
let sameAccount = true;
|
|
735
|
+
if (input.credentialsSource) {
|
|
736
|
+
const srcUuid = await accountUuidAt(join(input.credentialsSource, ".claude.json"));
|
|
737
|
+
const oldUuid = await accountUuidAt(join(runtimeDir, ".claude.json"));
|
|
738
|
+
if (srcUuid && oldUuid && srcUuid !== oldUuid) sameAccount = false;
|
|
739
|
+
}
|
|
740
|
+
const preserveFiles = sameAccount ? [".claude.json", ".credentials.json", "backups"] : [];
|
|
720
741
|
for (const name of preserveFiles) {
|
|
721
742
|
const oldPath = join(runtimeDir, name);
|
|
722
743
|
const newPath = join(tmpDir, name);
|
|
@@ -770,6 +791,24 @@ async function credentialsExpiresAt(path: string): Promise<number> {
|
|
|
770
791
|
}
|
|
771
792
|
}
|
|
772
793
|
|
|
794
|
+
/**
|
|
795
|
+
* Read `oauthAccount.accountUuid` from a `.claude.json` at `path`. Returns
|
|
796
|
+
* undefined when the file is missing, unparseable, or carries no account —
|
|
797
|
+
* callers treat "unknown" as "don't make account-based decisions".
|
|
798
|
+
*
|
|
799
|
+
* Sibling of `readAccountUuid` in credentials-sync.ts (dir-based); keep the
|
|
800
|
+
* schema (`oauthAccount.accountUuid`) in sync if it ever changes.
|
|
801
|
+
*/
|
|
802
|
+
async function accountUuidAt(path: string): Promise<string | undefined> {
|
|
803
|
+
try {
|
|
804
|
+
const raw = await readFile(path, "utf8");
|
|
805
|
+
const parsed = JSON.parse(raw) as { oauthAccount?: { accountUuid?: string } };
|
|
806
|
+
return parsed?.oauthAccount?.accountUuid;
|
|
807
|
+
} catch {
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
773
812
|
function collectProfileMcps(
|
|
774
813
|
profile: ResolvedProfile,
|
|
775
814
|
agent: AgentKind,
|
|
@@ -900,7 +939,36 @@ async function overlaySourceState(targetDir: string, sourceDir: string): Promise
|
|
|
900
939
|
// a shared one that gets clobbered when 2 profiles run concurrently.
|
|
901
940
|
const isCopyFile = name === ".credentials.json" || isLegacyClaudeJson;
|
|
902
941
|
|
|
903
|
-
if (existingType === "other" && !isCopyFile)
|
|
942
|
+
if (existingType === "other" && !isCopyFile) {
|
|
943
|
+
// Account-identity guard: .claude.json starts life as a symlink into the
|
|
944
|
+
// source dir, but Claude Code's atomic rewrite (tmp → rename) replaces it
|
|
945
|
+
// with a local FILE owned by whichever account last logged in here. Since
|
|
946
|
+
// runtime dirs are keyed by profile (not account), a different authmux
|
|
947
|
+
// account launching the same profile used to find its fresh tokens paired
|
|
948
|
+
// with the OLD account's identity — booting into the login flow every time
|
|
949
|
+
// the two accounts alternated on a profile. When the uuids differ, re-seed
|
|
950
|
+
// identity from the source so it follows CLAUDE_CONFIG_DIR.
|
|
951
|
+
//
|
|
952
|
+
// Trade-off: the swap replaces the whole file, so the OLD account's
|
|
953
|
+
// per-profile session state (projects list etc.) in this runtime is
|
|
954
|
+
// discarded — acceptable, since it belongs to a different account.
|
|
955
|
+
if (name === ".claude.json") {
|
|
956
|
+
const srcUuid = await accountUuidAt(sourcePath);
|
|
957
|
+
const dstUuid = await accountUuidAt(targetPath);
|
|
958
|
+
if (srcUuid && dstUuid && srcUuid !== dstUuid) {
|
|
959
|
+
try {
|
|
960
|
+
// Copy to a sibling tmp + atomic rename — never leaves a window
|
|
961
|
+
// where .claude.json is missing/partial while a concurrent claude
|
|
962
|
+
// process might read or atomically rewrite it.
|
|
963
|
+
const { copyFile } = await import("node:fs/promises");
|
|
964
|
+
const tmp = `${targetPath}.cue-swap.${process.pid}`;
|
|
965
|
+
await copyFile(sourcePath, tmp);
|
|
966
|
+
await rename(tmp, targetPath);
|
|
967
|
+
} catch { /* non-fatal — keep existing file */ }
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
continue; // cue override — don't touch
|
|
971
|
+
}
|
|
904
972
|
|
|
905
973
|
if (existingType === "symlink" || (existingType === "other" && isCopyFile)) {
|
|
906
974
|
// Replace if it points elsewhere (e.g. previous account on cache hit).
|
|
@@ -1057,6 +1125,41 @@ async function buildClaudeSettings(
|
|
|
1057
1125
|
if (Object.keys(mergedHooks).length > 0) {
|
|
1058
1126
|
settings.hooks = mergedHooks;
|
|
1059
1127
|
}
|
|
1128
|
+
|
|
1129
|
+
// Surface an allowlisted subset of profile.env into settings.json `env` so
|
|
1130
|
+
// Claude Code's cost/runtime knobs actually reach the session. profile.env is
|
|
1131
|
+
// otherwise consumed only for MCP-placeholder substitution (mcp-materializer)
|
|
1132
|
+
// and never reaches the agent process. We allowlist deliberately: profile.env
|
|
1133
|
+
// also holds secret references like "${AWS_SECRET_ACCESS_KEY}" that must NOT
|
|
1134
|
+
// be written into settings.json. Gated to claude-code (these keys are
|
|
1135
|
+
// Claude-Code-specific; codex uses its own config). Set in `core` so it fans
|
|
1136
|
+
// out to every inheriting profile — e.g. CLAUDE_CODE_SUBAGENT_MODEL pins
|
|
1137
|
+
// subagents to Sonnet, ~50-60% cheaper than Opus on file-read/grep/review.
|
|
1138
|
+
if (agent === "claude-code") {
|
|
1139
|
+
// Allowlist of Claude-Code cost/runtime knobs that may flow from profile.env
|
|
1140
|
+
// into settings.json. To surface a new one, append its key here.
|
|
1141
|
+
const CLAUDE_RUNTIME_ENV_KEYS = [
|
|
1142
|
+
"CLAUDE_CODE_SUBAGENT_MODEL", // run Task/Agent subagents on a cheaper model
|
|
1143
|
+
];
|
|
1144
|
+
// Preserve any account-level env from credentialsSource (spread in via
|
|
1145
|
+
// baseSettings above); profile-declared keys overlay it (profile is more
|
|
1146
|
+
// specific). Skip unset values and unresolved placeholders — the `${`
|
|
1147
|
+
// check is deliberately conservative: any "${...}"-shaped value is treated
|
|
1148
|
+
// as an unresolved secret reference and dropped, never written out.
|
|
1149
|
+
const runtimeEnv: Record<string, string> = {
|
|
1150
|
+
...((settings.env as Record<string, string> | undefined) ?? {}),
|
|
1151
|
+
};
|
|
1152
|
+
for (const key of CLAUDE_RUNTIME_ENV_KEYS) {
|
|
1153
|
+
const val = profile.env?.[key];
|
|
1154
|
+
if (typeof val === "string" && val.length > 0 && !val.includes("${")) {
|
|
1155
|
+
runtimeEnv[key] = val;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (Object.keys(runtimeEnv).length > 0) {
|
|
1159
|
+
settings.env = runtimeEnv;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1060
1163
|
return JSON.stringify(settings, null, 2);
|
|
1061
1164
|
}
|
|
1062
1165
|
|
|
@@ -297,3 +297,41 @@ describe("renderRouter zombie compaction", () => {
|
|
|
297
297
|
expect(md).toContain("Rarely-used skills (3)");
|
|
298
298
|
});
|
|
299
299
|
});
|
|
300
|
+
|
|
301
|
+
describe("renderRouter capability cap", () => {
|
|
302
|
+
function manySkills(n: number): ParsedSkill[] {
|
|
303
|
+
return Array.from({ length: n }, (_, i) =>
|
|
304
|
+
parseSkillFromContent(
|
|
305
|
+
`cat/skill-${i}`,
|
|
306
|
+
`---\ndescription: Does useful thing number ${i} for the pipeline workflow.\n---`,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
test("no cap → every capability row renders", () => {
|
|
312
|
+
const md = renderRouter(manySkills(60));
|
|
313
|
+
const rows = md.split("\n").filter((l) => l.startsWith("| Does useful thing"));
|
|
314
|
+
expect(rows.length).toBe(60);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("cap truncates capability rows and emits an on-demand note", () => {
|
|
318
|
+
const md = renderRouter(manySkills(60), { maxCapabilityRows: 50 });
|
|
319
|
+
const rows = md.split("\n").filter((l) => l.startsWith("| Does useful thing"));
|
|
320
|
+
expect(rows.length).toBe(50);
|
|
321
|
+
expect(md).toMatch(/\+10 more skills/);
|
|
322
|
+
expect(md).toContain("loadable via the Skill tool on demand");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("cap is a no-op when row count is at or below the cap", () => {
|
|
326
|
+
const md = renderRouter(manySkills(50), { maxCapabilityRows: 50 });
|
|
327
|
+
expect(md).not.toMatch(/more skill.* loadable via the Skill tool/);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("manual persona_routing rows survive the cap", () => {
|
|
331
|
+
const md = renderRouter(manySkills(60), {
|
|
332
|
+
maxCapabilityRows: 50,
|
|
333
|
+
overrides: [{ capability: "ALWAYS-KEEP this manual row", skill: "x/manual" }],
|
|
334
|
+
});
|
|
335
|
+
expect(md).toContain("ALWAYS-KEEP this manual row");
|
|
336
|
+
});
|
|
337
|
+
});
|
package/src/lib/skill-router.ts
CHANGED
|
@@ -324,6 +324,16 @@ export interface RouterRenderOptions {
|
|
|
324
324
|
* push the file past Claude Code's 40KB perf-warning threshold.
|
|
325
325
|
*/
|
|
326
326
|
omitTriggerPhrases?: boolean;
|
|
327
|
+
/**
|
|
328
|
+
* Cap the number of rows in the capability table. On heavy profiles (60+
|
|
329
|
+
* skills) the auto-built capability table is the single largest block in
|
|
330
|
+
* the materialized CLAUDE.md. Beyond the cap, the lowest-signal skills
|
|
331
|
+
* (weak metadata first) drop their capability row and collapse into a
|
|
332
|
+
* single "+N more loadable on demand" note — they stay listed under
|
|
333
|
+
* "Available Skills" and remain invokable via the Skill tool. Manual
|
|
334
|
+
* `persona_routing:` rows are always kept. 0 / undefined → no cap.
|
|
335
|
+
*/
|
|
336
|
+
maxCapabilityRows?: number;
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
/**
|
|
@@ -370,15 +380,23 @@ export function renderRouter(
|
|
|
370
380
|
// Capability rows: any skill with a capability blurb OR explicit
|
|
371
381
|
// when_to_invoke entries. We surface up to 3 task-shapes per skill;
|
|
372
382
|
// skills with only a capability blurb render a single "any X work" row.
|
|
373
|
-
const capabilityRows: {
|
|
383
|
+
const capabilityRows: {
|
|
384
|
+
task: string;
|
|
385
|
+
skill: string;
|
|
386
|
+
manual?: boolean;
|
|
387
|
+
note?: string;
|
|
388
|
+
quality?: ParseQuality;
|
|
389
|
+
}[] = [];
|
|
374
390
|
for (const s of activeSkills) {
|
|
375
391
|
if (s.whenToInvoke.length > 0) {
|
|
376
|
-
|
|
377
|
-
|
|
392
|
+
// Two task-shapes per skill is enough to prime routing; a third row
|
|
393
|
+
// rarely adds signal and inflates the table on heavy profiles.
|
|
394
|
+
for (const task of s.whenToInvoke.slice(0, 2)) {
|
|
395
|
+
capabilityRows.push({ task, skill: s.name, quality: s.quality });
|
|
378
396
|
}
|
|
379
397
|
} else if (s.capability) {
|
|
380
398
|
const summary = truncate(s.capability, 70);
|
|
381
|
-
capabilityRows.push({ task: summary, skill: s.name });
|
|
399
|
+
capabilityRows.push({ task: summary, skill: s.name, quality: s.quality });
|
|
382
400
|
}
|
|
383
401
|
}
|
|
384
402
|
|
|
@@ -411,6 +429,44 @@ export function renderRouter(
|
|
|
411
429
|
}
|
|
412
430
|
}
|
|
413
431
|
|
|
432
|
+
// Capability-table cap. On heavy profiles this table is the largest block
|
|
433
|
+
// in the materialized CLAUDE.md. Keep manual (persona_routing) rows plus the
|
|
434
|
+
// highest-signal skills; collapse the overflow into a single on-demand note.
|
|
435
|
+
// The dropped skills stay listed under "Available Skills" and invokable via
|
|
436
|
+
// the Skill tool — only their capability hint is omitted.
|
|
437
|
+
let capOverflowSkills = 0;
|
|
438
|
+
const cap = options.maxCapabilityRows ?? 0;
|
|
439
|
+
if (cap > 0 && capabilityRows.length > cap) {
|
|
440
|
+
const qualityRank: Record<ParseQuality, number> = { good: 0, partial: 1, none: 2 };
|
|
441
|
+
const ordered = capabilityRows
|
|
442
|
+
.map((row, idx) => ({ row, idx }))
|
|
443
|
+
.sort((a, b) => {
|
|
444
|
+
// Manual rows first (always kept), then by metadata quality, then
|
|
445
|
+
// stable on original order so a skill's rows stay together.
|
|
446
|
+
const am = a.row.manual ? 0 : 1;
|
|
447
|
+
const bm = b.row.manual ? 0 : 1;
|
|
448
|
+
if (am !== bm) return am - bm;
|
|
449
|
+
const aq = qualityRank[a.row.quality ?? "none"];
|
|
450
|
+
const bq = qualityRank[b.row.quality ?? "none"];
|
|
451
|
+
if (aq !== bq) return aq - bq;
|
|
452
|
+
return a.idx - b.idx;
|
|
453
|
+
});
|
|
454
|
+
const keptSet = new Set(ordered.slice(0, cap).map((e) => e.idx));
|
|
455
|
+
const keptSkills = new Set<string>();
|
|
456
|
+
const droppedSkills = new Set<string>();
|
|
457
|
+
capabilityRows.forEach((row, idx) => {
|
|
458
|
+
if (keptSet.has(idx)) keptSkills.add(row.skill);
|
|
459
|
+
});
|
|
460
|
+
capabilityRows.forEach((row, idx) => {
|
|
461
|
+
if (!keptSet.has(idx) && !keptSkills.has(row.skill)) droppedSkills.add(row.skill);
|
|
462
|
+
});
|
|
463
|
+
capOverflowSkills = droppedSkills.size;
|
|
464
|
+
// Re-filter in original order so kept rows read naturally.
|
|
465
|
+
const filtered = capabilityRows.filter((_, idx) => keptSet.has(idx));
|
|
466
|
+
capabilityRows.length = 0;
|
|
467
|
+
capabilityRows.push(...filtered);
|
|
468
|
+
}
|
|
469
|
+
|
|
414
470
|
// Tail: skills that yielded nothing or are missing on disk. Zombies that
|
|
415
471
|
// already match the "no metadata" criterion are deduped here so they don't
|
|
416
472
|
// appear in both the "Other skills" tail AND the "Rarely-used" tail.
|
|
@@ -448,6 +504,11 @@ export function renderRouter(
|
|
|
448
504
|
"These wrap the underlying tools with prompt-enhancement, house " +
|
|
449
505
|
"style, or correct CLI invocations. Freestyling around them produces " +
|
|
450
506
|
"worse output.\n\n";
|
|
507
|
+
if (capOverflowSkills > 0) {
|
|
508
|
+
out +=
|
|
509
|
+
`_+${capOverflowSkills} more skill${capOverflowSkills === 1 ? "" : "s"} in this profile — ` +
|
|
510
|
+
"listed under \"Available Skills\" and loadable via the Skill tool on demand._\n\n";
|
|
511
|
+
}
|
|
451
512
|
}
|
|
452
513
|
|
|
453
514
|
if (triggerRows.length > 0 && !options.omitTriggerPhrases) {
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# 🏛️ eu-tender-research
|
|
2
|
-
|
|
3
|
-
EU funding research for **agentic AI companies in Slovakia or Hungary**. Two jobs in one
|
|
4
|
-
profile: find public-procurement **tenders** to bid on, and map the **grants, cheap loans,
|
|
5
|
-
and venture capital** a company can apply for. Built so the second run is faster, cheaper,
|
|
6
|
-
and more accurate than the first.
|
|
7
|
-
|
|
8
|
-
## Two channels, two skills
|
|
9
|
-
|
|
10
|
-
| You want to... | Skill | Source |
|
|
11
|
-
|---|---|---|
|
|
12
|
-
| Find EU tenders to bid on | `eu-funding/ted-tender-search` | Official TED API (free, no key) |
|
|
13
|
-
| Find grants / loans / VC for a company | `eu-funding/hu-grant-finder` | palyazat.gov.hu, NKFIH, MFB, Hiventures, EIC |
|
|
14
|
-
|
|
15
|
-
Both skills carry the **verified recipes** so the agent doesn't rediscover them:
|
|
16
|
-
the TED API endpoint + Expert-Search query language, the Hungarian funding ladder,
|
|
17
|
-
the canonical sources, and the eligibility gotchas that disqualify fastest.
|
|
18
|
-
|
|
19
|
-
## The funding channels (the #1 thing users confuse)
|
|
20
|
-
|
|
21
|
-
- **Tender / közbeszerzés** — TED procurement: you *sell* services to the state.
|
|
22
|
-
- **Grant / támogatás** — GINOP/DIMOP: non-repayable project money.
|
|
23
|
-
- **Loan / hitel** — MFB / GINOP 1.4.x: cheap (often 0%) but you repay.
|
|
24
|
-
- **VC / kockázati tőke** — Hiventures (GINOP 2.5.1), EIC: equity investment.
|
|
25
|
-
|
|
26
|
-
## Gotchas baked into the persona
|
|
27
|
-
|
|
28
|
-
- **Closed-year wall** — most KKV grants/loans need ≥1 closed business year. An egyéni
|
|
29
|
-
vállalkozó's closed tax year counts; don't found a fresh Kft and reset the clock.
|
|
30
|
-
- **Region split** — DIMOP/GINOP: Budapest (`/C`) vs every other county (`/B`, favoured).
|
|
31
|
-
- **Timing** — frames close on *forráskimerülés*, often before the deadline; re-check live.
|
|
32
|
-
- **VC is not free** — equity + TRL6 product; GINOP 2.5.1 excludes "only software" firms.
|
|
33
|
-
- **Deep-tech ≠ SaaS** — an LLM-wrapper won't win EIC; Hungary's route is EIC Pre-Accelerator.
|
|
34
|
-
|
|
35
|
-
## MCPs
|
|
36
|
-
|
|
37
|
-
`ted-eu` (free single-notice fetch), `lightpanda` (page render), `gbrain` (track
|
|
38
|
-
shortlisted opportunities across sessions). No paid scraper, no API token. Apify is
|
|
39
|
-
re-addable if ever wanted: `cue mcps add apify-ted-eu` (needs `APIFY_API_TOKEN`).
|
|
40
|
-
|
|
41
|
-
## Use it
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
cue use eu-tender-research
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Then ask things like *"find open IT tenders from Hungarian buyers"* or *"what grants can a
|
|
48
|
-
new Hungarian AI company apply for?"* — the matching skill activates and runs the verified flow.
|
|
Binary file
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
name: eu-tender-research
|
|
2
|
-
icon: "🏛️"
|
|
3
|
-
iconImage: "logo.png"
|
|
4
|
-
description: "EU funding research for agentic AI companies in Slovakia or Hungary: TED tenders + grants/loans/VC (GINOP, DIMOP, Hiventures, EIC). Find, filter, assess eligibility, draft briefs."
|
|
5
|
-
agents: [claude-code]
|
|
6
|
-
inherits: core
|
|
7
|
-
skills:
|
|
8
|
-
local:
|
|
9
|
-
- eu-funding/ted-tender-search
|
|
10
|
-
- eu-funding/hu-grant-finder
|
|
11
|
-
- eu-funding/grant-outreach
|
|
12
|
-
- research/find-skills
|
|
13
|
-
- research/defuddle
|
|
14
|
-
- research/trendradar
|
|
15
|
-
- gstack/scrape
|
|
16
|
-
- gstack/document-generate
|
|
17
|
-
- gstack/make-pdf
|
|
18
|
-
- content/article-writer
|
|
19
|
-
mcps:
|
|
20
|
-
- lightpanda
|
|
21
|
-
- gbrain
|
|
22
|
-
- trendradar
|
|
23
|
-
env:
|
|
24
|
-
EU_TENDER_HQ_COUNTRIES: "SK,HU"
|
|
25
|
-
TED_API_BASE: "https://api.ted.europa.eu/v3"
|
|
26
|
-
persona: |
|
|
27
|
-
You run EU funding research for agentic AI companies headquartered in
|
|
28
|
-
Slovakia (SK) or Hungary (HU). Two jobs: (1) surface TED procurement
|
|
29
|
-
tenders these firms can bid on, and (2) map the grants, cheap loans, and
|
|
30
|
-
venture capital they can apply for. Rank by fit and eligibility, then turn
|
|
31
|
-
winners into a clean brief.
|
|
32
|
-
|
|
33
|
-
## Funding channels (DON'T conflate them, this is the #1 user confusion)
|
|
34
|
-
|
|
35
|
-
| Channel | What it is | Lead skill |
|
|
36
|
-
|---|---|---|
|
|
37
|
-
| Tender / közbeszerzés | TED procurement, you *sell* to the state | `eu-funding/ted-tender-search` |
|
|
38
|
-
| Grant / támogatás | GINOP/DIMOP, non-repayable | `eu-funding/hu-grant-finder` |
|
|
39
|
-
| Cheap loan / hitel | MFB / GINOP 1.4.x, 0% but repay | `eu-funding/hu-grant-finder` |
|
|
40
|
-
| Venture capital | Hiventures (GINOP 2.5.1), EIC, equity | `eu-funding/hu-grant-finder` |
|
|
41
|
-
|
|
42
|
-
The two `eu-funding/*` skills carry the verified recipes (TED API query
|
|
43
|
-
language, the Hungarian funding ladder, the sources, the eligibility
|
|
44
|
-
gotchas). Read the matching skill first, don't rediscover the endpoint or
|
|
45
|
-
the gotchas from scratch.
|
|
46
|
-
|
|
47
|
-
## Gotchas that gate everything (check before detailing amounts)
|
|
48
|
-
|
|
49
|
-
- **Closed-year wall** — most KKV grants/loans need >=1 closed business
|
|
50
|
-
year + 1 employee; a brand-new company can't apply day one. An egyeni
|
|
51
|
-
vallalkozo's closed tax year counts, so don't found a fresh Kft and
|
|
52
|
-
reset that clock.
|
|
53
|
-
- **Region split** — DIMOP/GINOP is Budapest (`/C`) vs every other county
|
|
54
|
-
(`/B`, "less developed"); wealthy counties are in the favoured `/B`.
|
|
55
|
-
- **Timing** — frames close on forraskimerules, often before the deadline;
|
|
56
|
-
re-check the live active list at palyazat.gov.hu the same day.
|
|
57
|
-
- **VC is not free** — Hiventures/EIC take equity, need a TRL6 working
|
|
58
|
-
product, and GINOP 2.5.1 excludes "only software" firms.
|
|
59
|
-
- **Deep-tech vs SaaS** — an LLM-wrapper is SaaS, not deep tech; EIC wants
|
|
60
|
-
breakthrough. Hungary is a "widening" country, so EIC Pre-Accelerator is
|
|
61
|
-
the realistic stepping stone.
|
|
62
|
-
|
|
63
|
-
## Data sources (route by need)
|
|
64
|
-
|
|
65
|
-
| Need | Tool |
|
|
66
|
-
|-------------------------------------------------------------------|-------------------------------|
|
|
67
|
-
| Search TED notices (CPV, country, deadline, value) — free, no key | Official TED API, `${TED_API_BASE}/notices/search` (POST) |
|
|
68
|
-
| Fetch one notice in full by publication number | Same TED API: `query: "publication-number IN (123456-2025)"` |
|
|
69
|
-
| Render a TED portal page that the API can't reach | `lightpanda` MCP, gstack/scrape |
|
|
70
|
-
| Strip a notice page to clean text | research/defuddle |
|
|
71
|
-
| Track shortlisted tenders + buyer contacts across sessions | `gbrain` MCP |
|
|
72
|
-
| Spot which procurement themes are heating up | research/trendradar |
|
|
73
|
-
| Draft a bid summary / capability statement | content/article-writer |
|
|
74
|
-
| Export the brief as a shareable doc/PDF | gstack/document-generate, gstack/make-pdf |
|
|
75
|
-
|
|
76
|
-
## Defaults
|
|
77
|
-
|
|
78
|
-
- **Fit before volume.** A relevant notice an SK/HU agentic-AI firm can win
|
|
79
|
-
beats ten generic IT tenders. Filter hard on CPV codes for software, AI,
|
|
80
|
-
data, and consulting services (72xxxxxx, 48xxxxxx, 73xxxxxx), buyer
|
|
81
|
-
country, deadline still open, and eligibility (no incumbent lock-in).
|
|
82
|
-
- **Source every notice.** Each tender in a brief carries its TED
|
|
83
|
-
publication number (e.g. `123456-2025`), buyer, country, value, deadline,
|
|
84
|
-
and the live notice URL. No number, not in the brief.
|
|
85
|
-
- **Rank, don't dump.** When you produce a list of tenders (3+), run
|
|
86
|
-
`/roi-estimator` so each row carries a fit/value tag the user can sort by.
|
|
87
|
-
- **Both bid angles.** A notice can fit either as the prime bidder or as a
|
|
88
|
-
subcontractor to a local integrator — flag which when it's not obvious.
|
|
89
|
-
- **Verify before claiming done.** Confirm a notice is still open and the
|
|
90
|
-
publication number resolves before putting it in front of the user.
|
|
91
|
-
|
|
92
|
-
## Free stack, no credentials
|
|
93
|
-
|
|
94
|
-
Everything here runs without an API key or paid scraper:
|
|
95
|
-
|
|
96
|
-
- **Search** is the official EU TED API at `${TED_API_BASE}/notices/search`
|
|
97
|
-
(POST JSON `{query, fields, limit, scope:"ACTIVE"}`). The Expert Search
|
|
98
|
-
query language filters on `classification-cpv`, `place-of-performance`,
|
|
99
|
-
`deadline-date`, `notice-type`, etc. No key needed.
|
|
100
|
-
Example: `classification-cpv IN (72000000) AND place-of-performance IN (SVK HUN)`.
|
|
101
|
-
- **Single-notice detail** comes from the same API:
|
|
102
|
-
`query: "publication-number IN (123456-2025)"`, `scope: "ALL"`. No MCP needed.
|
|
103
|
-
- **Page scraping** uses `lightpanda` + `defuddle` locally — no cloud
|
|
104
|
-
scraper (Apify, Firecrawl, etc.) required.
|
|
105
|
-
- **Why no TED MCP:** the `ted-eu` gateway exposes 22 tools for 2 useful
|
|
106
|
-
ones, and its search is an NL-router black box. The API above is precise,
|
|
107
|
-
verified, and zero-bloat. Re-add it only if you want NL queries:
|
|
108
|
-
`cue mcps add ted-eu` (or `apify-ted-eu`, needs `APIFY_API_TOKEN`).
|