cue-ai 0.5.0 → 0.7.0

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 (154) hide show
  1. package/README.md +757 -110
  2. package/package.json +5 -5
  3. package/profiles/README.md +12 -12
  4. package/profiles/SCHEMA.md +31 -3
  5. package/profiles/_cache/README.md +1 -1
  6. package/profiles/_types.ts +26 -1
  7. package/profiles/backend/profile.yaml +1 -0
  8. package/profiles/career/profile.yaml +13 -0
  9. package/profiles/core/profile.yaml +76 -9
  10. package/profiles/creative-media/README.md +1 -1
  11. package/profiles/cybersecurity/profile.yaml +779 -756
  12. package/profiles/ecc/profile.yaml +39 -0
  13. package/profiles/event-design/profile.yaml +10 -0
  14. package/profiles/fleet-control/README.md +1 -1
  15. package/profiles/frontend/profile.yaml +14 -0
  16. package/profiles/full/README.md +1 -1
  17. package/profiles/go-api/profile.yaml +1 -0
  18. package/profiles/marketing/profile.yaml +12 -1
  19. package/profiles/predict-everything/profile.yaml +9 -0
  20. package/profiles/rust/profile.yaml +22 -3
  21. package/profiles/rust-cli/profile.yaml +14 -0
  22. package/profiles/rust-core/profile.yaml +35 -0
  23. package/profiles/rust-embedded/profile.yaml +11 -0
  24. package/profiles/rust-ffi/profile.yaml +13 -0
  25. package/profiles/rust-game/profile.yaml +11 -0
  26. package/profiles/rust-wasm/profile.yaml +11 -0
  27. package/profiles/rust-web/profile.yaml +17 -0
  28. package/profiles/schema.json +44 -4
  29. package/profiles/trendradar/profile.yaml +11 -0
  30. package/resources/mcps/README.md +39 -164
  31. package/resources/mcps/configs/claude.sanitized.json +55 -0
  32. package/resources/mcps/configs/claude_runtime.sanitized.json +47 -0
  33. package/resources/skills/README.md +70 -113
  34. package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +43 -0
  35. package/resources/skills/skills/meta/acpx/SKILL.md +78 -0
  36. package/resources/skills/skills/meta/cue-usage/SKILL.md +24 -0
  37. package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +24 -0
  38. package/resources/skills/skills/predict-everything/mirofish/SKILL.md +75 -0
  39. package/resources/skills/skills/research/trendradar/SKILL.md +88 -0
  40. package/resources/skills/skills/rust/async-tokio/SKILL.md +27 -0
  41. package/resources/skills/skills/rust/axum-api/SKILL.md +38 -0
  42. package/resources/skills/skills/rust/bacon-watch/SKILL.md +24 -0
  43. package/resources/skills/skills/rust/bevy/SKILL.md +43 -0
  44. package/resources/skills/skills/rust/bindgen/SKILL.md +39 -0
  45. package/resources/skills/skills/rust/cargo-audit/SKILL.md +26 -0
  46. package/resources/skills/skills/rust/cargo-basics/SKILL.md +28 -0
  47. package/resources/skills/skills/rust/cargo-chef/SKILL.md +43 -0
  48. package/resources/skills/skills/rust/cargo-edit/SKILL.md +26 -0
  49. package/resources/skills/skills/rust/cargo-expand/SKILL.md +24 -0
  50. package/resources/skills/skills/rust/cargo-flamegraph/SKILL.md +26 -0
  51. package/resources/skills/skills/rust/cargo-fuzz/SKILL.md +34 -0
  52. package/resources/skills/skills/rust/cargo-hack/SKILL.md +26 -0
  53. package/resources/skills/skills/rust/cargo-msrv/SKILL.md +30 -0
  54. package/resources/skills/skills/rust/cargo-mutants/SKILL.md +26 -0
  55. package/resources/skills/skills/rust/cargo-nextest/SKILL.md +24 -0
  56. package/resources/skills/skills/rust/cargo-readme/SKILL.md +36 -0
  57. package/resources/skills/skills/rust/cbindgen/SKILL.md +41 -0
  58. package/resources/skills/skills/rust/chisel-tool/SKILL.md +32 -0
  59. package/resources/skills/skills/rust/clap-cli/SKILL.md +44 -0
  60. package/resources/skills/skills/rust/clippy-and-fmt/SKILL.md +25 -0
  61. package/resources/skills/skills/rust/cross-compile/SKILL.md +26 -0
  62. package/resources/skills/skills/rust/embedded/SKILL.md +33 -0
  63. package/resources/skills/skills/rust/error-handling/SKILL.md +32 -0
  64. package/resources/skills/skills/rust/just-runner/SKILL.md +26 -0
  65. package/resources/skills/skills/rust/mdbook/SKILL.md +25 -0
  66. package/resources/skills/skills/rust/napi-rs/SKILL.md +32 -0
  67. package/resources/skills/skills/rust/no-std/SKILL.md +42 -0
  68. package/resources/skills/skills/rust/property-testing/SKILL.md +35 -0
  69. package/resources/skills/skills/rust/pyo3/SKILL.md +40 -0
  70. package/resources/skills/skills/rust/ratatui-tui/SKILL.md +36 -0
  71. package/resources/skills/skills/rust/release-plz/SKILL.md +27 -0
  72. package/resources/skills/skills/rust/reqwest/SKILL.md +37 -0
  73. package/resources/skills/skills/rust/sccache/SKILL.md +28 -0
  74. package/resources/skills/skills/rust/serde/SKILL.md +30 -0
  75. package/resources/skills/skills/rust/snapshot-testing/SKILL.md +30 -0
  76. package/resources/skills/skills/rust/sqlx-cli/SKILL.md +33 -0
  77. package/resources/skills/skills/rust/tracing/SKILL.md +36 -0
  78. package/resources/skills/skills/rust/typos-spellcheck/SKILL.md +31 -0
  79. package/resources/skills/skills/rust/uniffi/SKILL.md +38 -0
  80. package/resources/skills/skills/rust/wasm-rust/SKILL.md +27 -0
  81. package/resources/skills/skills/security/agentshield/SKILL.md +119 -0
  82. package/src/commands/_index.ts +47 -3
  83. package/src/commands/cli.test.ts +192 -0
  84. package/src/commands/cli.ts +303 -0
  85. package/src/commands/current.ts +1 -1
  86. package/src/commands/debug.test.ts +62 -0
  87. package/src/commands/debug.ts +212 -0
  88. package/src/commands/discover.scoring.test.ts +216 -0
  89. package/src/commands/discover.test.ts +145 -0
  90. package/src/commands/discover.ts +2618 -0
  91. package/src/commands/eval-behavior.test.ts +56 -0
  92. package/src/commands/eval-behavior.ts +189 -0
  93. package/src/commands/eval.test.ts +102 -0
  94. package/src/commands/eval.ts +348 -0
  95. package/src/commands/evolve.ts +291 -0
  96. package/src/commands/failures.test.ts +78 -0
  97. package/src/commands/failures.ts +393 -0
  98. package/src/commands/feedback.ts +219 -0
  99. package/src/commands/init.ts +26 -0
  100. package/src/commands/launch.e2e.test.ts +9 -1
  101. package/src/commands/launch.ts +174 -11
  102. package/src/commands/lint-skill.ts +157 -0
  103. package/src/commands/marketplace.ts +763 -2
  104. package/src/commands/new.ts +1 -1
  105. package/src/commands/optimizer.ts +92 -28
  106. package/src/commands/profile-draft-skill.test.ts +96 -0
  107. package/src/commands/profile-draft-skill.ts +287 -0
  108. package/src/commands/profile-evolve.test.ts +126 -0
  109. package/src/commands/profile-evolve.ts +0 -0
  110. package/src/commands/profile-suggest.ts +223 -0
  111. package/src/commands/profile.ts +41 -0
  112. package/src/commands/quick.ts +2 -17
  113. package/src/commands/scan.ts +2 -2
  114. package/src/commands/score.ts +1 -1
  115. package/src/commands/share.ts +1 -1
  116. package/src/commands/sources.ts +2 -2
  117. package/src/commands/submit-profile.ts +262 -0
  118. package/src/commands/upgrade.ts +1 -1
  119. package/src/commands/use.ts +35 -5
  120. package/src/commands/validate.ts +1 -1
  121. package/src/index.ts +66 -0
  122. package/src/lib/analytics.ts +48 -2
  123. package/src/lib/claude-binary.ts +39 -0
  124. package/src/lib/cli-extractor.ts +77 -0
  125. package/src/lib/cluster-skills.test.ts +268 -0
  126. package/src/lib/cluster-skills.ts +290 -0
  127. package/src/lib/credentials-sync.test.ts +208 -0
  128. package/src/lib/credentials-sync.ts +205 -0
  129. package/src/lib/mcp-materializer.test.ts +1 -1
  130. package/src/lib/persona-playbooks.test.ts +111 -0
  131. package/src/lib/pr-poster.test.ts +243 -0
  132. package/src/lib/pr-poster.ts +285 -0
  133. package/src/lib/pr-throttle.test.ts +148 -0
  134. package/src/lib/pr-throttle.ts +209 -0
  135. package/src/lib/profile-generator.test.ts +1 -1
  136. package/src/lib/profile-generator.ts +2 -2
  137. package/src/lib/profile-linter.test.ts +6 -3
  138. package/src/lib/profile-linter.ts +71 -8
  139. package/src/lib/profile-loader.test.ts +1 -1
  140. package/src/lib/profile-loader.ts +16 -0
  141. package/src/lib/resolver-local.test.ts +1 -1
  142. package/src/lib/resolver-npx.test.ts +76 -1
  143. package/src/lib/resolver-npx.ts +35 -3
  144. package/src/lib/resolver-plugins.test.ts +1 -1
  145. package/src/lib/runtime-materializer.test.ts +191 -7
  146. package/src/lib/runtime-materializer.ts +310 -42
  147. package/src/lib/scan-plugins.test.ts +1 -1
  148. package/src/lib/skill-linter.test.ts +174 -0
  149. package/src/lib/skill-linter.ts +507 -0
  150. package/src/lib/skill-subset.test.ts +95 -0
  151. package/src/lib/skill-subset.ts +166 -0
  152. package/src/lib/star-prompt.ts +1 -1
  153. package/src/lib/uvx-installer.test.ts +229 -0
  154. package/src/lib/uvx-installer.ts +278 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * `cue submit-profile <path>` — fork opencue/cue, branch, commit the profile,
3
+ * open a PR. Lowers contribution friction for community profiles.
4
+ *
5
+ * Workflow:
6
+ * 1. Validate the input profile.yaml via the existing linter (no E-errors)
7
+ * 2. `gh repo fork opencue/cue --clone` into a tmp dir (or use existing fork)
8
+ * 3. Copy profile.yaml into profiles/<name>/ on a new branch
9
+ * 4. Commit + push + `gh pr create`
10
+ *
11
+ * Safety: dry-run by default. The user must pass --yes to actually push + PR.
12
+ */
13
+
14
+ import { existsSync, mkdtempSync, mkdirSync, copyFileSync, readFileSync } from "node:fs";
15
+ import { tmpdir, homedir } from "node:os";
16
+ import { join, basename, resolve } from "node:path";
17
+ import { spawnSync } from "node:child_process";
18
+
19
+ import { parse as parseYaml } from "yaml";
20
+
21
+ import { lintProfile, type ProfileLintResult } from "../lib/profile-linter";
22
+
23
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
24
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
25
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
26
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
27
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
28
+
29
+ const UPSTREAM_REPO = "opencue/cue";
30
+
31
+ interface ProfileMeta {
32
+ name: string;
33
+ description: string;
34
+ icon?: string;
35
+ inherits?: string;
36
+ }
37
+
38
+ function readProfileMeta(path: string): ProfileMeta {
39
+ const content = readFileSync(path, "utf8");
40
+ const parsed = parseYaml(content);
41
+ if (!parsed || typeof parsed !== "object") throw new Error(`${path} is not valid YAML`);
42
+ const { name, description, icon, inherits } = parsed as ProfileMeta;
43
+ if (!name) throw new Error(`profile.yaml missing required field: name`);
44
+ if (!description) throw new Error(`profile.yaml missing required field: description`);
45
+ if (!/^[a-z][a-z0-9-]{1,63}$/.test(name)) throw new Error(`profile name "${name}" must be kebab-case ([a-z][a-z0-9-]{1,63})`);
46
+ return { name, description, icon, inherits };
47
+ }
48
+
49
+ function run(cmd: string, args: string[], opts: { cwd?: string; inherit?: boolean } = {}): { ok: boolean; out: string; err: string } {
50
+ const res = spawnSync(cmd, args, {
51
+ cwd: opts.cwd,
52
+ encoding: "utf8",
53
+ stdio: opts.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
54
+ timeout: 60000,
55
+ });
56
+ return {
57
+ ok: res.status === 0,
58
+ out: res.stdout?.trim() ?? "",
59
+ err: res.stderr?.trim() ?? "",
60
+ };
61
+ }
62
+
63
+ function hasGh(): boolean {
64
+ return run("gh", ["--version"]).ok;
65
+ }
66
+
67
+ function ghUser(): string | null {
68
+ const r = run("gh", ["api", "user", "--jq", ".login"]);
69
+ return r.ok ? r.out : null;
70
+ }
71
+
72
+ function printLintReport(result: ProfileLintResult): boolean {
73
+ let hasErrors = false;
74
+ for (const issue of result.issues) {
75
+ if (issue.severity === "error") hasErrors = true;
76
+ const icon = issue.severity === "error" ? red("✗") : yellow("⚠");
77
+ process.stdout.write(` ${icon} ${issue.rule}: ${issue.message}\n`);
78
+ }
79
+ return !hasErrors;
80
+ }
81
+
82
+ export async function runCmd(args: string[]): Promise<number> {
83
+ if (args.includes("-h") || args.includes("--help") || args.length === 0) {
84
+ process.stdout.write(`cue submit-profile — open a PR adding a profile to ${UPSTREAM_REPO}
85
+
86
+ Usage:
87
+ cue submit-profile <path/to/profile.yaml> [--yes] [--dry-run] [--branch <name>]
88
+
89
+ Flow:
90
+ 1. Validate the profile (schema + linter)
91
+ 2. Fork ${UPSTREAM_REPO} (or use existing fork)
92
+ 3. Branch, copy profile.yaml to profiles/<name>/profile.yaml
93
+ 4. Commit, push, open PR via gh
94
+
95
+ Defaults to --dry-run. Pass --yes to actually fork + push + PR.
96
+
97
+ Examples:
98
+ cue submit-profile ./my-profile/profile.yaml
99
+ cue submit-profile ./my-profile/profile.yaml --yes
100
+ cue submit-profile ./my-profile/profile.yaml --yes --branch add-data-engineering-profile
101
+ `);
102
+ return 0;
103
+ }
104
+
105
+ const path = args.find(a => !a.startsWith("-"));
106
+ if (!path) {
107
+ process.stderr.write("Usage: cue submit-profile <path/to/profile.yaml>\n");
108
+ return 1;
109
+ }
110
+ if (!existsSync(path)) {
111
+ process.stderr.write(`File not found: ${path}\n`);
112
+ return 1;
113
+ }
114
+ const dryRun = !args.includes("--yes");
115
+ const branchIdx = args.indexOf("--branch");
116
+ const branchOverride = branchIdx >= 0 ? args[branchIdx + 1] : undefined;
117
+
118
+ // ── 1. Parse + validate ───────────────────────────────────────────────
119
+ let meta: ProfileMeta;
120
+ try {
121
+ meta = readProfileMeta(path);
122
+ } catch (e) {
123
+ process.stderr.write(red(`✗ Schema error: `) + (e as Error).message + "\n");
124
+ return 1;
125
+ }
126
+ process.stdout.write(`\n ${bold("Profile:")} ${meta.name} ${dim(meta.description)}\n`);
127
+
128
+ // ── 2. Run the linter (requires the file to be in profiles/<name>/) ───
129
+ // We stage to a tmp profiles dir + lint there so the resolver finds it.
130
+ const stagingRoot = mkdtempSync(join(tmpdir(), "cue-submit-"));
131
+ const profilesDir = join(stagingRoot, "profiles", meta.name);
132
+ mkdirSync(profilesDir, { recursive: true });
133
+ copyFileSync(resolve(path), join(profilesDir, "profile.yaml"));
134
+ process.stdout.write(` ${dim("Staged to:")} ${profilesDir}\n`);
135
+
136
+ process.stdout.write(`\n ${bold("Lint report:")}\n`);
137
+ const lintResult = await lintProfile(meta.name, { profilesDir: join(stagingRoot, "profiles") } as any).catch((e: Error) => {
138
+ process.stderr.write(red(`✗ Lint failed: `) + e.message + "\n");
139
+ return null;
140
+ });
141
+ if (!lintResult) return 1;
142
+
143
+ const lintOk = printLintReport(lintResult);
144
+ if (!lintOk) {
145
+ process.stderr.write(`\n ${red("✗ Fix the E-errors above before submitting.")}\n\n`);
146
+ return 1;
147
+ }
148
+ process.stdout.write(` ${green("✓ Lint passed")}\n`);
149
+
150
+ // ── 3. gh fork + branch + commit + PR ────────────────────────────────
151
+ if (!hasGh()) {
152
+ process.stderr.write(red("✗ gh CLI not found. Install: https://cli.github.com/\n"));
153
+ return 1;
154
+ }
155
+ const user = ghUser();
156
+ if (!user) {
157
+ process.stderr.write(red("✗ Not authenticated. Run: gh auth login\n"));
158
+ return 1;
159
+ }
160
+ process.stdout.write(` ${dim("gh user:")} ${user}\n`);
161
+
162
+ const branch = branchOverride ?? `add-profile-${meta.name}`;
163
+ const prTitle = `Add ${meta.icon ? meta.icon + " " : ""}${meta.name} profile`;
164
+ const prBody = `## What
165
+
166
+ Adds a new \`${meta.name}\` profile to cue.
167
+
168
+ **Description**: ${meta.description}
169
+ ${meta.inherits ? `**Inherits**: \`${meta.inherits}\`\n` : ""}
170
+
171
+ ## Why
172
+
173
+ [Briefly explain the use case — what kind of project benefits from this profile, and what's missing in the existing profiles that prompted you to create it.]
174
+
175
+ ## Validation
176
+
177
+ \`\`\`
178
+ cue validate ${meta.name}
179
+ \`\`\`
180
+
181
+ - ✅ Schema valid
182
+ - ✅ All skill / MCP / rule / command references resolve
183
+ - ✅ No W1-W5 hard warnings
184
+
185
+ ## Submitted via
186
+
187
+ \`cue submit-profile\` — happy to iterate on the profile based on review feedback.
188
+
189
+ ---
190
+
191
+ <sub>Opened with [\`cue submit-profile\`](https://github.com/opencue/cue/blob/main/src/commands/submit-profile.ts).</sub>`;
192
+
193
+ if (dryRun) {
194
+ process.stdout.write(`\n ${bold("─── DRY RUN ───")}\n`);
195
+ process.stdout.write(` ${dim("Would:")}\n`);
196
+ process.stdout.write(` 1. ${green("gh repo fork " + UPSTREAM_REPO + " --clone")} (if needed)\n`);
197
+ process.stdout.write(` 2. ${green(`git checkout -b ${branch}`)}\n`);
198
+ process.stdout.write(` 3. ${green(`cp ${path} profiles/${meta.name}/profile.yaml`)}\n`);
199
+ process.stdout.write(` 4. ${green(`git commit -m "feat: add ${meta.name} profile"`)}\n`);
200
+ process.stdout.write(` 5. ${green(`git push -u origin ${branch}`)}\n`);
201
+ process.stdout.write(` 6. ${green("gh pr create")}\n`);
202
+ process.stdout.write(`\n ${bold("PR Title:")} ${prTitle}\n`);
203
+ process.stdout.write(`\n ${bold("PR Body preview:")}\n${prBody.split("\n").map(l => " " + l).join("\n")}\n`);
204
+ process.stdout.write(`\n ${dim("(dry-run — pass --yes to execute)")}\n\n`);
205
+ return 0;
206
+ }
207
+
208
+ // ── Real execution path ──────────────────────────────────────────────
209
+ const forkDir = join(homedir(), ".cache", "cue", "submit-fork");
210
+ mkdirSync(join(homedir(), ".cache", "cue"), { recursive: true });
211
+
212
+ if (!existsSync(join(forkDir, ".git"))) {
213
+ process.stdout.write(`\n ${bold("Forking")} ${UPSTREAM_REPO}...\n`);
214
+ const fork = run("gh", ["repo", "fork", UPSTREAM_REPO, "--clone", forkDir, "--remote=true"], { inherit: true });
215
+ if (!fork.ok) {
216
+ process.stderr.write(red("✗ Fork failed\n"));
217
+ return 1;
218
+ }
219
+ } else {
220
+ process.stdout.write(` ${dim(`Reusing existing fork at ${forkDir}`)}\n`);
221
+ run("git", ["fetch", "upstream", "main"], { cwd: forkDir });
222
+ run("git", ["checkout", "main"], { cwd: forkDir });
223
+ run("git", ["reset", "--hard", "upstream/main"], { cwd: forkDir });
224
+ }
225
+
226
+ process.stdout.write(` ${bold("Creating branch:")} ${branch}\n`);
227
+ run("git", ["checkout", "-b", branch], { cwd: forkDir });
228
+ const destDir = join(forkDir, "profiles", meta.name);
229
+ mkdirSync(destDir, { recursive: true });
230
+ copyFileSync(resolve(path), join(destDir, "profile.yaml"));
231
+
232
+ run("git", ["add", "profiles/" + meta.name + "/profile.yaml"], { cwd: forkDir });
233
+ const commit = run("git", ["commit", "-m", `feat: add ${meta.name} profile`], { cwd: forkDir });
234
+ if (!commit.ok) {
235
+ process.stderr.write(red("✗ Commit failed: ") + commit.err + "\n");
236
+ return 1;
237
+ }
238
+
239
+ process.stdout.write(` ${bold("Pushing branch...")}\n`);
240
+ const push = run("git", ["push", "-u", "origin", branch], { cwd: forkDir, inherit: true });
241
+ if (!push.ok) {
242
+ process.stderr.write(red("✗ Push failed\n"));
243
+ return 1;
244
+ }
245
+
246
+ process.stdout.write(` ${bold("Opening PR...")}\n`);
247
+ const pr = run("gh", [
248
+ "pr", "create",
249
+ "--repo", UPSTREAM_REPO,
250
+ "--title", prTitle,
251
+ "--body", prBody,
252
+ "--head", `${user}:${branch}`,
253
+ ], { cwd: forkDir });
254
+ if (!pr.ok) {
255
+ process.stderr.write(red("✗ PR creation failed: ") + pr.err + "\n");
256
+ return 1;
257
+ }
258
+ process.stdout.write(`\n ${green("✓ PR opened:")} ${pr.out}\n\n`);
259
+ return 0;
260
+ }
261
+
262
+ export { runCmd as run };
@@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url";
12
12
 
13
13
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
14
14
  const REGISTRY_PATH = join(REPO_ROOT, "docs", "registry", "index.json");
15
- const REGISTRY_URL = "https://recodeee.github.io/cue/registry/index.json";
15
+ const REGISTRY_URL = "https://opencue.github.io/cue/registry/index.json";
16
16
  const UPGRADE_STATE = join(REPO_ROOT, "profiles", "_cache", "last-upgrade.json");
17
17
 
18
18
  interface RegistrySkill {
@@ -1,8 +1,38 @@
1
1
  /**
2
- * `soul use <profile>` — stub.
3
- * Real implementation belongs to agent A14 (materializer driver).
2
+ * `cue use <profile>` — pin a profile to the current directory.
3
+ *
4
+ * Writes `.cue-profile` in CWD (or $HOME with --global).
4
5
  */
5
- export async function run(_args: string[]): Promise<number> {
6
- process.stderr.write("[A14] not yet implemented (owned by agent A14)\n");
7
- return 1;
6
+
7
+ import { writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
10
+
11
+ import { listProfiles } from "../lib/profile-loader";
12
+
13
+ export async function run(args: string[]): Promise<number> {
14
+ const global = args.includes("--global") || args.includes("-g");
15
+ const profileName = args.find(a => !a.startsWith("-"));
16
+
17
+ if (!profileName) {
18
+ process.stderr.write("Usage: cue use <profile> [--global]\n");
19
+ const profiles = await listProfiles();
20
+ process.stderr.write(`\nAvailable: ${profiles.join(", ")}\n`);
21
+ return 1;
22
+ }
23
+
24
+ // Validate profile exists
25
+ const profiles = await listProfiles();
26
+ if (!profiles.includes(profileName)) {
27
+ process.stderr.write(`Profile "${profileName}" not found.\n`);
28
+ process.stderr.write(`Available: ${profiles.join(", ")}\n`);
29
+ return 1;
30
+ }
31
+
32
+ const target = global ? join(homedir(), ".cue-profile") : join(process.cwd(), ".cue-profile");
33
+ writeFileSync(target, profileName + "\n");
34
+
35
+ const scope = global ? "globally" : `in ${process.cwd()}`;
36
+ process.stdout.write(`✅ Now using "${profileName}" ${scope}\n`);
37
+ return 0;
8
38
  }
@@ -76,7 +76,7 @@ function printHelp(stream: Pick<NodeJS.WriteStream, "write"> = process.stdout):
76
76
  "",
77
77
  "Checks:",
78
78
  " schema validity, inheritance, local/npx/plugin skill resolution, MCP registry resolution",
79
- " W1-W4 warnings and E1-E3 lint errors",
79
+ " W1-W5 warnings and E1-E3 lint errors",
80
80
  "",
81
81
  ].join("\n"),
82
82
  );
package/src/index.ts CHANGED
@@ -132,9 +132,75 @@ function similarity(a: string, b: string): number {
132
132
  return (2 * matches) / (a.length - 1 + b.length - 1);
133
133
  }
134
134
 
135
+ // ---------------------------------------------------------------------------
136
+ // Auto-update check — runs at most once per 24h, non-blocking
137
+ // ---------------------------------------------------------------------------
138
+
139
+ async function checkForUpdate(currentVersion: string): Promise<void> {
140
+ const { existsSync, readFileSync: rf, writeFileSync: wf, mkdirSync } = await import("node:fs");
141
+ const { join } = await import("node:path");
142
+ const { homedir } = await import("node:os");
143
+
144
+ const configDir = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cue");
145
+ const checkFile = join(configDir, ".last-update-check");
146
+
147
+ // Only check once per 24 hours
148
+ if (existsSync(checkFile)) {
149
+ const last = parseInt(rf(checkFile, "utf8").trim(), 10) || 0;
150
+ if (Date.now() - last < 86400000) return;
151
+ }
152
+
153
+ // Fetch latest version from npm
154
+ const res = await fetch("https://registry.npmjs.org/cue-ai/latest", { signal: AbortSignal.timeout(3000) });
155
+ if (!res.ok) return;
156
+ const data = await res.json() as { version?: string };
157
+ const latest = data.version;
158
+ if (!latest) return;
159
+
160
+ // Save check timestamp
161
+ mkdirSync(configDir, { recursive: true });
162
+ wf(checkFile, String(Date.now()));
163
+
164
+ // Compare versions
165
+ if (latest === currentVersion) return;
166
+ const [cMaj, cMin, cPatch] = currentVersion.split(".").map(Number);
167
+ const [lMaj, lMin, lPatch] = latest.split(".").map(Number);
168
+ if (lMaj! < cMaj! || (lMaj === cMaj && lMin! < cMin!) || (lMaj === cMaj && lMin === cMin && lPatch! <= cPatch!)) return;
169
+
170
+ // Prompt user
171
+ process.stderr.write(`\n ⬆️ Update available: ${currentVersion} → ${latest}\n`);
172
+ process.stderr.write(` Run: npm install -g cue-ai\n\n`);
173
+
174
+ // Auto-install prompt (only in interactive TTY)
175
+ if (process.stdin.isTTY && process.stdout.isTTY) {
176
+ const readline = await import("node:readline");
177
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
178
+ const answer = await new Promise<string>((resolve) => {
179
+ rl.question(" Install now? [Y/n] ", (a) => { rl.close(); resolve(a); });
180
+ // Auto-accept after 5 seconds
181
+ setTimeout(() => { rl.close(); resolve("y"); }, 5000);
182
+ });
183
+ if (!answer || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
184
+ process.stderr.write(" 📦 Updating...\n");
185
+ const { spawnSync } = await import("node:child_process");
186
+ const result = spawnSync("npm", ["install", "-g", "cue-ai"], { encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"] });
187
+ if (result.status === 0) {
188
+ process.stderr.write(` ✅ Updated to ${latest}\n\n`);
189
+ } else {
190
+ process.stderr.write(` ⚠️ Update failed. Run manually: npm install -g cue-ai\n\n`);
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+
135
198
  async function main(argv: string[]): Promise<number> {
136
199
  const args = argv.slice(2);
137
200
 
201
+ // Non-blocking update check (runs in background, shows prompt if outdated)
202
+ checkForUpdate(readVersion()).catch(() => {});
203
+
138
204
  if (args.length === 0) {
139
205
  // Show status dashboard by default (like `git status`)
140
206
  const statusCmd = COMMANDS["status"];
@@ -13,6 +13,39 @@ const ANALYTICS_PATH = join(
13
13
  "analytics.jsonl",
14
14
  );
15
15
 
16
+ /**
17
+ * Session-summary hook (resources/hooks/session-summary.sh) appends one line
18
+ * per session end here. Read it as a secondary source for sessions counts so
19
+ * usage stats reflect real hook data, not just the launch-time analytics path.
20
+ */
21
+ const SESSION_LOG_PATH = join(
22
+ process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"),
23
+ "cue",
24
+ "session-log.jsonl",
25
+ );
26
+
27
+ interface SessionLogEntry {
28
+ ts: string;
29
+ cwd: string;
30
+ profile: string;
31
+ session_id: string;
32
+ }
33
+
34
+ function readSessionLog(since?: Date): SessionLogEntry[] {
35
+ if (!existsSync(SESSION_LOG_PATH)) return [];
36
+ const out: SessionLogEntry[] = [];
37
+ for (const line of readFileSync(SESSION_LOG_PATH, "utf8").split("\n")) {
38
+ if (!line.trim()) continue;
39
+ try {
40
+ const e = JSON.parse(line) as SessionLogEntry;
41
+ if (!e.profile) continue;
42
+ if (since && new Date(e.ts) < since) continue;
43
+ out.push(e);
44
+ } catch { /* skip malformed */ }
45
+ }
46
+ return out;
47
+ }
48
+
16
49
  export interface SessionEvent {
17
50
  ts: string;
18
51
  event: "start" | "end" | "skill_hit";
@@ -100,11 +133,24 @@ export interface ProfileStats {
100
133
 
101
134
  export function computeStats(since?: Date): ProfileStats[] {
102
135
  const events = readEvents(since);
103
- const map = new Map<string, { sessions: number; total_s: number; last: string }>();
136
+ const map = new Map<string, { sessions: number; total_s: number; last: string; seenIds: Set<string> }>();
104
137
 
105
138
  for (const e of events) {
106
139
  if (e.event !== "start") continue;
107
- const entry = map.get(e.profile) ?? { sessions: 0, total_s: 0, last: "" };
140
+ const entry = map.get(e.profile) ?? { sessions: 0, total_s: 0, last: "", seenIds: new Set<string>() };
141
+ entry.sessions++;
142
+ if (e.ts > entry.last) entry.last = e.ts;
143
+ map.set(e.profile, entry);
144
+ }
145
+
146
+ // Fold in hook-emitted session-log entries (Stop hook). Dedupe by session_id
147
+ // so a session that fires both the launch-time analytics and the Stop hook
148
+ // doesn't double-count. Entries without an id fall through as best-effort.
149
+ for (const e of readSessionLog(since)) {
150
+ const entry = map.get(e.profile) ?? { sessions: 0, total_s: 0, last: "", seenIds: new Set<string>() };
151
+ const key = e.session_id || `${e.ts}|${e.cwd}`;
152
+ if (entry.seenIds.has(key)) continue;
153
+ entry.seenIds.add(key);
108
154
  entry.sessions++;
109
155
  if (e.ts > entry.last) entry.last = e.ts;
110
156
  map.set(e.profile, entry);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Locate the real `claude` binary on PATH, skipping cue's shim.
3
+ *
4
+ * cue installs ~/.local/bin/claude as a bash one-liner that calls `cue launch
5
+ * claude`; shelling to that from within cue would recurse or trigger the picker.
6
+ *
7
+ * Lookup order:
8
+ * 1. $CUE_REAL_CLAUDE (explicit override)
9
+ * 2. $CLAUDE_CODE_EXECPATH (set by claude-code itself on subprocesses)
10
+ * 3. Walk $PATH, skipping any small bash shim that contains `cue launch`.
11
+ */
12
+
13
+ import { existsSync, readFileSync, statSync } from "node:fs";
14
+ import { join } from "node:path";
15
+
16
+ export function findRealClaudeBin(): string | null {
17
+ if (process.env.CUE_REAL_CLAUDE && existsSync(process.env.CUE_REAL_CLAUDE)) {
18
+ return process.env.CUE_REAL_CLAUDE;
19
+ }
20
+ if (process.env.CLAUDE_CODE_EXECPATH && existsSync(process.env.CLAUDE_CODE_EXECPATH)) {
21
+ return process.env.CLAUDE_CODE_EXECPATH;
22
+ }
23
+ const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
24
+ for (const dir of pathDirs) {
25
+ const candidate = join(dir, "claude");
26
+ if (!existsSync(candidate)) continue;
27
+ try {
28
+ const stat = statSync(candidate);
29
+ if (stat.size < 500) {
30
+ const content = readFileSync(candidate, "utf8");
31
+ if (/cue\s+launch/i.test(content)) continue;
32
+ }
33
+ return candidate;
34
+ } catch {
35
+ return candidate;
36
+ }
37
+ }
38
+ return null;
39
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Extract required-CLI list for a profile by parsing every skill's frontmatter
3
+ * `allowed-tools:`, Prerequisites section, and name. Delegates the actual
4
+ * single-skill parsing to commands/optimizer.ts (where the original logic
5
+ * already lives) so the two paths stay in lockstep.
6
+ *
7
+ * Glob entries like the "every skill" wildcard expand to every skill on disk
8
+ * — mirrors what the materializer / optimizer panel does so `cue cli list
9
+ * full` actually sees the underlying skills rather than returning 0.
10
+ */
11
+
12
+ import { existsSync, readdirSync } from "node:fs";
13
+ import { join, resolve, dirname } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { homedir } from "node:os";
16
+
17
+ import { loadProfile } from "./profile-loader";
18
+ import { extractCLIsFromSkill } from "../commands/optimizer";
19
+
20
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
21
+ const SKILLS_ROOT = join(REPO_ROOT, "resources", "skills", "skills");
22
+ const HOME_SKILLS = join(homedir(), ".claude", "skills");
23
+
24
+ export interface CliRequirement {
25
+ cli: string;
26
+ skills: string[];
27
+ }
28
+
29
+ /** Expand wildcard ids (containing `*`) to concrete skill ids on disk. */
30
+ function expandGlob(id: string): string[] {
31
+ if (!id.includes("*")) return [id];
32
+ const out: string[] = [];
33
+ try {
34
+ for (const cat of readdirSync(SKILLS_ROOT)) {
35
+ const catPath = join(SKILLS_ROOT, cat);
36
+ try {
37
+ for (const skill of readdirSync(catPath)) {
38
+ if (existsSync(join(catPath, skill, "SKILL.md"))) {
39
+ out.push(`${cat}/${skill}`);
40
+ }
41
+ }
42
+ } catch {}
43
+ }
44
+ } catch {}
45
+ try {
46
+ for (const skill of readdirSync(HOME_SKILLS)) {
47
+ if (!out.some((e) => e.endsWith(`/${skill}`))) out.push(skill);
48
+ }
49
+ } catch {}
50
+ return out;
51
+ }
52
+
53
+ export async function requiredClisFor(profileName: string): Promise<CliRequirement[]> {
54
+ const profile = await loadProfile(profileName);
55
+ const byCli = new Map<string, string[]>();
56
+ const add = (cli: string, skillId: string) => {
57
+ const list = byCli.get(cli) ?? [];
58
+ list.push(skillId);
59
+ byCli.set(cli, list);
60
+ };
61
+
62
+ for (const skill of profile.skills.local) {
63
+ for (const id of expandGlob(skill.id)) {
64
+ const slug = id.split("/").pop() ?? id;
65
+ for (const cli of extractCLIsFromSkill(slug)) add(cli, id);
66
+ }
67
+ }
68
+ for (const entry of profile.skills.npx) {
69
+ for (const slug of entry.skills) {
70
+ for (const cli of extractCLIsFromSkill(slug)) add(cli, `${entry.repo}:${slug}`);
71
+ }
72
+ }
73
+
74
+ return [...byCli.entries()]
75
+ .map(([cli, skills]) => ({ cli, skills }))
76
+ .sort((a, b) => b.skills.length - a.skills.length);
77
+ }