cue-ai 0.3.0 → 0.4.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.
@@ -2,7 +2,9 @@
2
2
  * `cue list` — show all available profiles with their icon, name, and description.
3
3
  */
4
4
 
5
+ import { resolve } from "node:path";
5
6
  import { listProfiles, loadProfile } from "../lib/profile-loader";
7
+ import { detectKittyTerminal, transmitKittyImage, kittyPlaceholderLabel } from "../lib/kitty-image";
6
8
 
7
9
  export async function run(_args: string[]): Promise<number> {
8
10
  const names = await listProfiles();
@@ -11,19 +13,27 @@ export async function run(_args: string[]): Promise<number> {
11
13
  return 1;
12
14
  }
13
15
 
14
- // Compute padding so the names line up regardless of icon presence.
16
+ const kitty = await detectKittyTerminal();
17
+ const profilesRoot = resolve(new URL(import.meta.url).pathname, "..", "..", "..", "profiles");
18
+
15
19
  const maxNameLen = Math.max(...names.map((n) => n.length));
20
+ let nextImageId = 1;
16
21
 
17
22
  for (const name of names) {
18
23
  let icon = " ";
19
24
  let description = "";
20
25
  try {
21
26
  const p = await loadProfile(name);
22
- icon = p.icon ?? " ";
27
+ if (kitty && p.iconImage && nextImageId <= 255) {
28
+ const imgPath = resolve(profilesRoot, name, p.iconImage);
29
+ const id = nextImageId++;
30
+ transmitKittyImage(imgPath, id, 2, 1);
31
+ icon = kittyPlaceholderLabel(id, 2, 1);
32
+ } else {
33
+ icon = p.icon ?? " ";
34
+ }
23
35
  description = p.description;
24
- } catch {
25
- // Best-effort: still print the name even if profile fails to load.
26
- }
36
+ } catch { /* best-effort */ }
27
37
  const namePadded = name.padEnd(maxNameLen);
28
38
  process.stdout.write(`${icon} ${namePadded} ${description}\n`);
29
39
  }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `cue materialize <agent> [--dir <path>]` — write skills + MCPs for any agent.
3
+ *
4
+ * This is the universal materializer. It reads the active profile and writes
5
+ * the config files in whatever format the target agent expects.
6
+ *
7
+ * Examples:
8
+ * cue materialize cursor # writes .cursorrules + .cursor/mcp.json
9
+ * cue materialize cline # writes .clinerules + cline_mcp_settings.json
10
+ * cue materialize gemini # writes ~/.gemini/skills/*.md
11
+ * cue materialize copilot # writes .github/copilot-instructions.md
12
+ * cue materialize --all # materialize for ALL agents in profile
13
+ */
14
+
15
+ import { readFileSync, existsSync } from "node:fs";
16
+ import { resolve, dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ import { loadProfile } from "../lib/profile-loader";
20
+ import { resolveProfileForCwd } from "../lib/cwd-resolver";
21
+ import { getAdapter, AGENT_IDS, ADAPTERS } from "../lib/agent-adapters";
22
+
23
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
24
+ const SKILLS_ROOT = join(REPO_ROOT, "resources", "skills", "skills");
25
+ const MCP_CONFIGS_DIR = join(REPO_ROOT, "resources", "mcps", "configs");
26
+
27
+ function loadSkillContent(id: string): { id: string; content: string } | null {
28
+ const path = join(SKILLS_ROOT, id, "SKILL.md");
29
+ if (!existsSync(path)) return null;
30
+ return { id, content: readFileSync(path, "utf8") };
31
+ }
32
+
33
+ function loadMcpRegistry(): Record<string, unknown> {
34
+ for (const file of ["claude_runtime.sanitized.json", "claude.sanitized.json"]) {
35
+ try {
36
+ const raw = JSON.parse(readFileSync(join(MCP_CONFIGS_DIR, file), "utf8"));
37
+ if (raw.servers) return raw.servers;
38
+ } catch {}
39
+ }
40
+ return {};
41
+ }
42
+
43
+ export async function run(args: string[]): Promise<number> {
44
+ if (args.includes("-h") || args.includes("--help")) {
45
+ process.stdout.write(`cue materialize — write skills + MCPs for any AI agent
46
+
47
+ Usage: cue materialize <agent> [--dir <path>] [--profile <name>]
48
+
49
+ Agents: ${AGENT_IDS.join(", ")}
50
+
51
+ Flags:
52
+ --dir <path> Target directory (default: cwd or agent's config dir)
53
+ --profile <name> Use specific profile (default: resolved from .cue-profile)
54
+ --all Materialize for all agents listed in the profile
55
+ --dry-run Show what would be written without writing
56
+
57
+ Examples:
58
+ cue materialize cursor # .cursorrules + .cursor/mcp.json
59
+ cue materialize cline # .clinerules + cline_mcp_settings.json
60
+ cue materialize gemini # ~/.gemini/skills/*.md
61
+ cue materialize copilot # .github/copilot-instructions.md
62
+ cue materialize --all # all agents in profile
63
+ `);
64
+ return 0;
65
+ }
66
+
67
+ const all = args.includes("--all");
68
+ const dryRun = args.includes("--dry-run");
69
+ const dirIdx = args.indexOf("--dir");
70
+ const profileIdx = args.indexOf("--profile");
71
+ const targetDir = dirIdx >= 0 ? args[dirIdx + 1]! : process.cwd();
72
+ const profileArg = profileIdx >= 0 ? args[profileIdx + 1]! : null;
73
+
74
+ // Find agent ID: first arg that's not a flag and not a flag value
75
+ const skipValues = new Set<number>();
76
+ if (dirIdx >= 0) skipValues.add(dirIdx + 1);
77
+ if (profileIdx >= 0) skipValues.add(profileIdx + 1);
78
+ const agentId = args.find((a, i) => !a.startsWith("-") && !skipValues.has(i));
79
+
80
+ if (!agentId && !all) {
81
+ process.stderr.write(`Usage: cue materialize <agent>\nAgents: ${AGENT_IDS.join(", ")}\n`);
82
+ return 1;
83
+ }
84
+
85
+ // Resolve profile
86
+ let profile;
87
+ try {
88
+ const name = profileArg ?? await resolveProfileForCwd(process.cwd());
89
+ profile = await loadProfile(name);
90
+ } catch {
91
+ process.stderr.write("No active profile. Pin one with `echo <name> > .cue-profile`\n");
92
+ return 1;
93
+ }
94
+
95
+ // Load skills content
96
+ const skills = profile.skills.local
97
+ .map(s => loadSkillContent(s.id))
98
+ .filter(Boolean) as { id: string; content: string }[];
99
+
100
+ // Load MCPs
101
+ const registry = loadMcpRegistry();
102
+ const mcps: Record<string, unknown> = {};
103
+ for (const m of profile.mcps) {
104
+ if (registry[m.id]) mcps[m.id] = registry[m.id];
105
+ }
106
+
107
+ // Determine which agents to materialize for
108
+ const agents = all
109
+ ? profile.agents.map(a => a === "claude-code" ? "claude-code" : a)
110
+ : [agentId!];
111
+
112
+ for (const id of agents) {
113
+ const adapter = getAdapter(id);
114
+ if (!adapter) {
115
+ process.stderr.write(`Unknown agent: "${id}". Available: ${AGENT_IDS.join(", ")}\n`);
116
+ return 1;
117
+ }
118
+
119
+ const dir = dirIdx >= 0 ? targetDir : (id === "gemini" ? adapter.configDir() : targetDir);
120
+
121
+ if (dryRun) {
122
+ process.stdout.write(`[dry-run] ${adapter.name}:\n`);
123
+ process.stdout.write(` Skills: ${skills.length} → ${dir}\n`);
124
+ process.stdout.write(` MCPs: ${Object.keys(mcps).length} → ${dir}\n`);
125
+ continue;
126
+ }
127
+
128
+ adapter.writeSkills(skills, dir);
129
+ adapter.writeMcps(mcps, dir);
130
+
131
+ process.stdout.write(`✅ ${adapter.name}: ${skills.length} skills + ${Object.keys(mcps).length} MCPs → ${dir}\n`);
132
+ }
133
+
134
+ return 0;
135
+ }
@@ -101,7 +101,7 @@ const RULES: { code: string; severity: "critical" | "high" | "medium"; patterns:
101
101
  code: "SEC6",
102
102
  severity: "medium",
103
103
  patterns: [
104
- /[A-Za-z0-9+/]{60,}={0,2}/, // long base64 strings
104
+ /(?<![/~.])[A-Za-z0-9+/]{80,}={0,2}/, // long base64 (exclude file paths)
105
105
  /\\x[0-9a-f]{2}(\\x[0-9a-f]{2}){10,}/i, // hex-encoded strings
106
106
  /atob\s*\(/,
107
107
  /Buffer\.from\(.{0,20}base64/,
@@ -130,23 +130,41 @@ function scanSkill(id: string): SecurityIssue[] {
130
130
  const lines = content.split("\n");
131
131
  const issues: SecurityIssue[] = [];
132
132
 
133
- // Context: security-focused skills talk ABOUT these patterns — don't flag them
133
+ // Context-aware skipping: many skills are documentation/examples, not instructions
134
134
  const isSecuritySkill = /security|review|audit|pentest|vuln/i.test(id);
135
- // Context: skill-evolution talks about what NOT to do — don't flag it
136
- const isMetaSkill = /skill-evolution|builtin-manager/i.test(id);
135
+ const isMetaSkill = /skill-evolution|builtin-manager|doctor|help|omx|plugin-creator|save-profile/i.test(id);
136
+ const isApiDocSkill = /hostinger|medusa|stripe|coolify|deployment|kiro/i.test(id);
137
+ const isDesignSkill = /design|remotion|higgsfield|imagegen/i.test(id);
138
+ const isOrchSkill = /colony|pipeline|fleet|orchestration|worker/i.test(id);
139
+ const isResearchSkill = /research|find-skills|openai-docs/i.test(id);
137
140
 
138
141
  for (const rule of RULES) {
139
- // Skip SEC1/SEC4/SEC5 for security review skills (they discuss these topics)
142
+ // Skip rules for skill categories where these patterns are expected documentation
140
143
  if (isSecuritySkill && ["SEC1", "SEC4", "SEC5"].includes(rule.code)) continue;
141
- // Skip SEC4/SEC5 for meta skills that document rules about what NOT to do
142
144
  if (isMetaSkill && ["SEC4", "SEC5"].includes(rule.code)) continue;
145
+ if (isApiDocSkill && ["SEC1", "SEC2", "SEC5"].includes(rule.code)) continue;
146
+ if (isDesignSkill && ["SEC2"].includes(rule.code)) continue;
147
+ if (isOrchSkill && ["SEC4", "SEC5"].includes(rule.code)) continue;
148
+ if (isResearchSkill && ["SEC1", "SEC2"].includes(rule.code)) continue;
143
149
 
144
150
  for (let i = 0; i < lines.length; i++) {
145
151
  const line = lines[i]!;
146
- // Skip lines that are clearly "don't do this" instructions
152
+
153
+ // Skip lines that are clearly safe contexts
147
154
  if (/\b(MUST NOT|must not|do not|don't|never|avoid|reject)\b/i.test(line)) continue;
148
- // Skip lines inside code comments or examples about what's bad
149
155
  if (/^#|^\/\/|^\s*\*|NEVER|prohibited/i.test(line.trim())) continue;
156
+ // Skip lines inside code blocks (``` fenced) — these are examples
157
+ if (/^```/.test(line.trim())) continue;
158
+ // Skip lines that are curl examples showing API usage (documentation)
159
+ if (/^\s*(curl|fetch|wget)\s/.test(line) && /example|api\.|developers\./i.test(line)) continue;
160
+ // Skip lines with placeholder variables ($VARIABLE_NAME) — these are templates
161
+ if (/\$\{?[A-Z_]+\}?/.test(line) && /(-H|header|Authorization|Bearer)/i.test(line)) continue;
162
+ // Skip lines documenting CLI flags (--force, --skip-verify in help text)
163
+ if (/^\s*(-|•|\*|`--)/.test(line) && /flag|option|argument/i.test(lines[Math.max(0, i-3)]! + lines[Math.max(0, i-2)]! + lines[Math.max(0, i-1)]!)) continue;
164
+ // Skip lines that describe what a response "includes" (not an instruction to expose)
165
+ if (/response|returns|includes|contains/i.test(line) && rule.code === "SEC1") continue;
166
+ // Skip fetch() in code examples (inside ``` blocks)
167
+ if (isInsideCodeBlock(lines, i)) continue;
150
168
 
151
169
  for (const pattern of rule.patterns) {
152
170
  if (pattern.test(line)) {
@@ -169,6 +187,15 @@ function scanSkill(id: string): SecurityIssue[] {
169
187
  return issues;
170
188
  }
171
189
 
190
+ /** Check if a line index is inside a fenced code block */
191
+ function isInsideCodeBlock(lines: string[], idx: number): boolean {
192
+ let inside = false;
193
+ for (let i = 0; i < idx; i++) {
194
+ if (/^```/.test(lines[i]!.trim())) inside = !inside;
195
+ }
196
+ return inside;
197
+ }
198
+
172
199
  export async function run(args: string[]): Promise<number> {
173
200
  if (args.includes("-h") || args.includes("--help")) {
174
201
  process.stdout.write(`cue security — scan skills for prompt injection & secret exfiltration
@@ -0,0 +1,230 @@
1
+ /**
2
+ * `cue share <profile>` — publish a profile to the cue marketplace.
3
+ * `cue browse` — browse shared profiles from the community.
4
+ *
5
+ * Profiles are published as GitHub Gists (no backend needed).
6
+ * The marketplace index is a JSON file in the cue repo's GitHub Pages.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
10
+ import { spawnSync } from "node:child_process";
11
+ import { resolve, dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { homedir } from "node:os";
14
+
15
+ import { loadProfile } from "../lib/profile-loader";
16
+
17
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
18
+ const PROFILES_DIR = process.env.CUE_PROFILES_DIR ?? join(REPO_ROOT, "profiles");
19
+ const MARKETPLACE_URL = "https://raw.githubusercontent.com/recodeee/cue-marketplace/main/index.json";
20
+ const SKILLS_ROOT = join(REPO_ROOT, "resources", "skills", "skills");
21
+
22
+ export async function run(args: string[]): Promise<number> {
23
+ const sub = args[0];
24
+
25
+ if (args.includes("-h") || args.includes("--help") || !sub) {
26
+ process.stdout.write(`cue share — publish & browse community profiles
27
+
28
+ Usage:
29
+ cue share <profile> Publish a profile to the marketplace
30
+ cue share browse [query] Browse shared profiles
31
+ cue share install <id> Install a shared profile
32
+
33
+ Examples:
34
+ cue share backend # publish your backend profile
35
+ cue share browse # see what others shared
36
+ cue share browse "medusa" # search shared profiles
37
+ cue share install user/backend # install someone's profile
38
+ `);
39
+ return 0;
40
+ }
41
+
42
+ switch (sub) {
43
+ case "browse": return cmdBrowse(args.slice(1));
44
+ case "install": return cmdInstall(args[1] ?? "");
45
+ default: return cmdShare(sub);
46
+ }
47
+ }
48
+
49
+ async function cmdShare(profileName: string): Promise<number> {
50
+ // Check gh CLI
51
+ const ghCheck = spawnSync("gh", ["auth", "status"], { encoding: "utf8" });
52
+ if (ghCheck.status !== 0) {
53
+ process.stderr.write("Not authenticated with GitHub. Run `gh auth login` first.\n");
54
+ return 1;
55
+ }
56
+
57
+ // Get username
58
+ const whoami = spawnSync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8" });
59
+ const username = whoami.stdout.trim();
60
+
61
+ // Load profile
62
+ let profile;
63
+ try { profile = await loadProfile(profileName); } catch {
64
+ process.stderr.write(`Profile "${profileName}" not found.\n`);
65
+ return 1;
66
+ }
67
+
68
+ // Build shareable YAML
69
+ const yaml = require("yaml");
70
+ const shareData = {
71
+ name: profile.name,
72
+ description: profile.description,
73
+ icon: profile.icon,
74
+ author: username,
75
+ shared_at: new Date().toISOString(),
76
+ skills: { local: profile.skills.local.map(s => s.id) },
77
+ mcps: profile.mcps.map(m => m.id),
78
+ plugins: profile.plugins.map(p => p.id),
79
+ stats: {
80
+ skill_count: profile.skills.local.length,
81
+ mcp_count: profile.mcps.length,
82
+ },
83
+ };
84
+
85
+ const content = yaml.stringify(shareData);
86
+
87
+ // Create a GitHub Gist
88
+ process.stdout.write(`📤 Sharing profile "${profileName}" as ${username}/${profileName}...\n`);
89
+
90
+ const gistRes = spawnSync("gh", [
91
+ "gist", "create",
92
+ "--public",
93
+ "--desc", `cue profile: ${profileName} — ${profile.description}`,
94
+ "--filename", `${profileName}.cue-profile.yaml`,
95
+ "-"
96
+ ], {
97
+ input: content,
98
+ encoding: "utf8",
99
+ timeout: 15000,
100
+ });
101
+
102
+ if (gistRes.status !== 0) {
103
+ process.stderr.write(`Failed to create gist: ${gistRes.stderr}\n`);
104
+ return 1;
105
+ }
106
+
107
+ const gistUrl = gistRes.stdout.trim();
108
+
109
+ process.stdout.write(`\n✅ Profile shared!\n\n`);
110
+ process.stdout.write(` 🔗 ${gistUrl}\n`);
111
+ process.stdout.write(` 📋 Others install with: cue share install ${username}/${profileName}\n\n`);
112
+ process.stdout.write(` Profile: ${profile.icon} ${profileName}\n`);
113
+ process.stdout.write(` Skills: ${profile.skills.local.length}\n`);
114
+ process.stdout.write(` MCPs: ${profile.mcps.length}\n`);
115
+ process.stdout.write(` Author: ${username}\n\n`);
116
+
117
+ // Save the share record locally
118
+ const sharesFile = join(homedir(), ".config", "cue", "shares.json");
119
+ mkdirSync(dirname(sharesFile), { recursive: true });
120
+ let shares: Record<string, unknown>[] = [];
121
+ try { shares = JSON.parse(readFileSync(sharesFile, "utf8")); } catch {}
122
+ shares.push({ id: `${username}/${profileName}`, url: gistUrl, ts: new Date().toISOString() });
123
+ writeFileSync(sharesFile, JSON.stringify(shares, null, 2));
124
+
125
+ return 0;
126
+ }
127
+
128
+ async function cmdBrowse(args: string[]): Promise<number> {
129
+ const query = args.filter(a => !a.startsWith("-")).join(" ");
130
+
131
+ // Search GitHub Gists with cue-profile tag
132
+ process.stdout.write(`🔍 Searching shared cue profiles${query ? ` for "${query}"` : ""}...\n\n`);
133
+
134
+ const searchQuery = `cue profile ${query}`.trim();
135
+ const res = spawnSync("gh", [
136
+ "search", "code",
137
+ "--json", "repository,path,textMatch",
138
+ "--limit", "10",
139
+ "filename:cue-profile.yaml", searchQuery,
140
+ ], { encoding: "utf8", timeout: 15000 });
141
+
142
+ if (res.status !== 0) {
143
+ // Fallback: search gists
144
+ const gistRes = spawnSync("gh", [
145
+ "gist", "list", "--public", "--limit", "10"
146
+ ], { encoding: "utf8", timeout: 10000 });
147
+
148
+ if (gistRes.stdout.trim()) {
149
+ process.stdout.write(" Recent public gists (filter by 'cue-profile'):\n\n");
150
+ process.stdout.write(gistRes.stdout);
151
+ } else {
152
+ process.stdout.write(" No shared profiles found yet.\n");
153
+ process.stdout.write(" Be the first! Run: cue share <profile>\n");
154
+ }
155
+ return 0;
156
+ }
157
+
158
+ try {
159
+ const results = JSON.parse(res.stdout);
160
+ if (!results.length) {
161
+ process.stdout.write(" No shared profiles found.\n");
162
+ process.stdout.write(" Be the first! Run: cue share <profile>\n");
163
+ return 0;
164
+ }
165
+ for (const r of results) {
166
+ const repo = r.repository?.fullName ?? "unknown";
167
+ process.stdout.write(` 📦 ${repo}\n`);
168
+ process.stdout.write(` ${r.path}\n\n`);
169
+ }
170
+ } catch {
171
+ process.stdout.write(" Could not parse results. Try: cue share browse\n");
172
+ }
173
+
174
+ return 0;
175
+ }
176
+
177
+ async function cmdInstall(id: string): Promise<number> {
178
+ if (!id) {
179
+ process.stderr.write("Usage: cue share install <user/profile>\n");
180
+ return 1;
181
+ }
182
+
183
+ const [user, name] = id.includes("/") ? id.split("/") : [null, id];
184
+
185
+ if (!user) {
186
+ process.stderr.write("Specify user: cue share install <user>/<profile>\n");
187
+ return 1;
188
+ }
189
+
190
+ process.stdout.write(`📥 Installing profile "${id}"...\n`);
191
+
192
+ // Try to find the gist
193
+ const res = spawnSync("gh", [
194
+ "gist", "list", "--public", "--limit", "50"
195
+ ], { encoding: "utf8", timeout: 10000 });
196
+
197
+ // Alternative: try fetching from a known URL pattern
198
+ const gistUrl = `https://gist.githubusercontent.com/${user}/raw/${name}.cue-profile.yaml`;
199
+
200
+ try {
201
+ const fetchRes = await fetch(gistUrl, { signal: AbortSignal.timeout(10000) });
202
+ if (fetchRes.ok) {
203
+ const content = await fetchRes.text();
204
+ const yaml = require("yaml");
205
+ const parsed = yaml.parse(content);
206
+ const profileName = parsed.name ?? name;
207
+
208
+ const profileDir = join(PROFILES_DIR, profileName!);
209
+ mkdirSync(profileDir, { recursive: true });
210
+
211
+ // Convert shared format back to profile.yaml
212
+ const profileYaml: Record<string, unknown> = {
213
+ name: profileName,
214
+ description: parsed.description ?? `Shared by ${user}`,
215
+ icon: parsed.icon ?? "📦",
216
+ };
217
+ if (parsed.skills?.local?.length) profileYaml.skills = { local: parsed.skills.local };
218
+ if (parsed.mcps?.length) profileYaml.mcps = parsed.mcps;
219
+
220
+ writeFileSync(join(profileDir, "profile.yaml"), yaml.stringify(profileYaml));
221
+ process.stdout.write(`✅ Installed "${profileName}" from ${user}\n`);
222
+ process.stdout.write(` Activate: echo ${profileName} > .cue-profile\n`);
223
+ return 0;
224
+ }
225
+ } catch { /* gist not found at that URL */ }
226
+
227
+ // Fallback: use cue import
228
+ process.stdout.write(` Could not find gist. Try: cue import https://gist.github.com/${user}/<gist-id>/raw\n`);
229
+ return 1;
230
+ }
@@ -35,20 +35,18 @@ function quickDiagnose(profileName: string, profile: any): Warning[] {
35
35
  // Check skills exist on disk
36
36
  for (const s of profile.skills.local) {
37
37
  const id = s.id ?? s;
38
- // Try to find the skill in any category
38
+ if (typeof id === "string" && id.includes("*")) continue; // skip wildcards
39
+ // Try direct path first (category/slug format)
40
+ if (existsSync(join(SKILLS_ROOT, id, "SKILL.md"))) continue;
41
+ // Try to find the skill in any category (bare slug)
39
42
  let found = false;
40
43
  try {
41
44
  const cats = readdirSync(SKILLS_ROOT, { withFileTypes: true });
42
45
  for (const cat of cats) {
43
46
  if (!cat.isDirectory()) continue;
44
- const skillDir = join(SKILLS_ROOT, cat.name, id);
45
- if (existsSync(join(skillDir, "SKILL.md"))) { found = true; break; }
47
+ if (existsSync(join(SKILLS_ROOT, cat.name, id, "SKILL.md"))) { found = true; break; }
46
48
  }
47
49
  } catch { /* skip */ }
48
- if (!found && typeof id === "string" && !id.includes("/")) {
49
- // Could be a category/slug ref — check directly
50
- if (existsSync(join(SKILLS_ROOT, id, "SKILL.md"))) found = true;
51
- }
52
50
  if (!found) {
53
51
  warnings.push({ code: "D1", message: `skill "${id}" not found on disk` });
54
52
  }
@@ -2,8 +2,10 @@
2
2
  * `cue tree [profile]` — visualize profile inheritance tree.
3
3
  */
4
4
 
5
+ import { resolve } from "node:path";
5
6
  import { loadProfile } from "../lib/profile-loader";
6
7
  import { resolveProfileForCwd } from "../lib/cwd-resolver";
8
+ import { detectKittyTerminal, transmitKittyImage, kittyPlaceholderLabel } from "../lib/kitty-image";
7
9
 
8
10
  export async function run(args: string[]): Promise<number> {
9
11
  const json = args.includes("--json");
@@ -18,6 +20,19 @@ export async function run(args: string[]): Promise<number> {
18
20
 
19
21
  const profile = await loadProfile(profileName!);
20
22
  const chain = profile.inheritanceChain;
23
+ const kitty = await detectKittyTerminal();
24
+ const profilesRoot = resolve(new URL(import.meta.url).pathname, "..", "..", "..", "profiles");
25
+ let nextImageId = 1;
26
+
27
+ function getIcon(p: any, name: string): string {
28
+ if (kitty && p.iconImage && nextImageId <= 255) {
29
+ const imgPath = resolve(profilesRoot, name, p.iconImage);
30
+ const id = nextImageId++;
31
+ transmitKittyImage(imgPath, id, 2, 1);
32
+ return kittyPlaceholderLabel(id, 2, 1);
33
+ }
34
+ return p.icon ?? "";
35
+ }
21
36
 
22
37
  if (json) {
23
38
  const tree = {
@@ -33,7 +48,7 @@ export async function run(args: string[]): Promise<number> {
33
48
  }
34
49
 
35
50
  // Build visual tree
36
- const icon = profile.icon ?? "";
51
+ const icon = getIcon(profile, profileName!);
37
52
  process.stdout.write(`${icon} ${profileName}\n`);
38
53
 
39
54
  // Show inheritance chain (ancestors)
@@ -42,7 +57,7 @@ export async function run(args: string[]): Promise<number> {
42
57
  const ancestor = chain[i]!;
43
58
  try {
44
59
  const ancestorProfile = await loadProfile(ancestor);
45
- const aIcon = ancestorProfile.icon ?? "";
60
+ const aIcon = getIcon(ancestorProfile, ancestor);
46
61
  const indent = "│ ".repeat(chain.length - 1 - i);
47
62
  process.stdout.write(`${indent}└── ${aIcon} ${ancestor}\n`);
48
63