@sayknow-cli/coding-agent 0.5.11 → 0.5.14

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 (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/cli/setup-cli.d.ts +1 -1
  3. package/dist/types/config/file-lock.d.ts +8 -0
  4. package/dist/types/config/model-registry.d.ts +1 -0
  5. package/dist/types/config/models-config-schema.d.ts +2 -0
  6. package/dist/types/defaults/skc-ui-skills.d.ts +12 -0
  7. package/dist/types/hooks/ui-skill-keywords.d.ts +84 -0
  8. package/dist/types/session-import/redact.d.ts +1 -1
  9. package/dist/types/setup/external-ui-skills.d.ts +37 -0
  10. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +13 -1
  11. package/dist/types/skc-runtime/ultragoal-succession.d.ts +251 -0
  12. package/dist/types/skc-runtime/workflow-placeholder.d.ts +6 -0
  13. package/package.json +7 -7
  14. package/src/cli/setup-cli.ts +22 -1
  15. package/src/cli/skills-cli.ts +29 -10
  16. package/src/commands/setup.ts +1 -0
  17. package/src/config/file-lock-gc.ts +39 -2
  18. package/src/config/file-lock.ts +22 -1
  19. package/src/config/model-registry.ts +116 -40
  20. package/src/config/models-config-schema.ts +1 -0
  21. package/src/defaults/skc/skills/ultragoal/SKILL.md +3 -0
  22. package/src/defaults/skc/ui-skills/LICENSE.appllama +21 -0
  23. package/src/defaults/skc/ui-skills/LICENSE.emilkowalski +21 -0
  24. package/src/defaults/skc/ui-skills/NOTICE.md +43 -0
  25. package/src/defaults/skc/ui-skills/animate/SKILL.md +536 -0
  26. package/src/defaults/skc/ui-skills/animation-vocabulary/SKILL.md +178 -0
  27. package/src/defaults/skc/ui-skills/apple-design/SKILL.md +285 -0
  28. package/src/defaults/skc/ui-skills/appllama-app-design-skill/SKILL.md +821 -0
  29. package/src/defaults/skc/ui-skills/ask-sonner/SKILL.md +157 -0
  30. package/src/defaults/skc/ui-skills/emil-design-eng/SKILL.md +671 -0
  31. package/src/defaults/skc/ui-skills/find-animation-opportunities/SKILL.md +137 -0
  32. package/src/defaults/skc/ui-skills/improve-animations/SKILL.md +305 -0
  33. package/src/defaults/skc/ui-skills/mobile-native/SKILL.md +308 -0
  34. package/src/defaults/skc/ui-skills/pick-ui-library/SKILL.md +82 -0
  35. package/src/defaults/skc/ui-skills/prototype/SKILL.md +300 -0
  36. package/src/defaults/skc/ui-skills/react-bits/SKILL.md +113 -0
  37. package/src/defaults/skc/ui-skills/review-animations/SKILL.md +312 -0
  38. package/src/defaults/skc-ui-skills.ts +108 -0
  39. package/src/extensibility/runtime-skill-discovery.ts +8 -2
  40. package/src/hooks/native-skill-hook.ts +14 -0
  41. package/src/hooks/ui-skill-keywords.ts +312 -0
  42. package/src/internal-urls/docs-index.generated.ts +1 -1
  43. package/src/prompts/agents/architect.md +1 -0
  44. package/src/prompts/agents/critic.md +1 -0
  45. package/src/prompts/agents/executor.md +1 -0
  46. package/src/prompts/agents/planner.md +1 -0
  47. package/src/prompts/system/system-prompt.md +1 -0
  48. package/src/prompts/tools/skill.md +2 -2
  49. package/src/sdk/session.ts +7 -5
  50. package/src/session-import/redact.ts +11 -3
  51. package/src/setup/external-ui-skills.ts +132 -0
  52. package/src/skc-runtime/state-writer.ts +24 -27
  53. package/src/skc-runtime/ultragoal-runtime.ts +125 -45
  54. package/src/skc-runtime/ultragoal-succession.ts +1667 -0
  55. package/src/skc-runtime/workflow-placeholder.ts +33 -0
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Handles `skc skills` for inspecting bundled workflow skill definitions.
2
+ * Handles `skc skills` for inspecting bundled workflow and UI skill definitions.
3
3
  */
4
4
  import {
5
5
  DEFAULT_SKC_DEFINITION_NAMES,
6
6
  type EmbeddedDefaultSkcSkill,
7
7
  getEmbeddedDefaultSkcSkills,
8
8
  } from "../defaults/skc-defaults";
9
+ import { getEmbeddedSkcUiSkills } from "../defaults/skc-ui-skills";
10
+ import type { Skill } from "../extensibility/skills";
9
11
 
10
12
  export type SkillsAction = "list" | "read";
11
13
 
@@ -28,17 +30,21 @@ interface SkillsReadEntry extends SkillsListEntry {
28
30
  content: string;
29
31
  }
30
32
 
31
- function getEmbeddedSkill(name: string): EmbeddedDefaultSkcSkill | undefined {
32
- return getEmbeddedDefaultSkcSkills().find(skill => skill.name === name);
33
+ function getEmbeddedSkill(name: string): EmbeddedDefaultSkcSkill | Skill | undefined {
34
+ return (
35
+ getEmbeddedDefaultSkcSkills().find(skill => skill.name === name) ??
36
+ getEmbeddedSkcUiSkills().find(skill => skill.name === name)
37
+ );
33
38
  }
34
39
 
35
40
  function listEmbeddedSkills(): SkillsListEntry[] {
36
- return getEmbeddedDefaultSkcSkills().map(skill => ({
41
+ const toEntry = (skill: EmbeddedDefaultSkcSkill | Skill): SkillsListEntry => ({
37
42
  name: skill.name,
38
43
  description: skill.description,
39
44
  path: skill.filePath,
40
45
  source: skill.source,
41
- }));
46
+ });
47
+ return [...getEmbeddedDefaultSkcSkills().map(toEntry), ...getEmbeddedSkcUiSkills().map(toEntry)];
42
48
  }
43
49
 
44
50
  function writeJson(value: unknown): void {
@@ -60,14 +66,27 @@ export async function runSkillsCommand(cmd: SkillsCommandArgs): Promise<void> {
60
66
 
61
67
  const name = cmd.name?.trim();
62
68
  if (!name) {
63
- process.stderr.write(`error: skill name is required for read (${DEFAULT_SKC_DEFINITION_NAMES.join(", ")})\n`);
69
+ process.stderr.write(
70
+ `error: skill name is required for read (${DEFAULT_SKC_DEFINITION_NAMES.join(", ")}, or a bundled UI skill such as emil-design-eng)\n`,
71
+ );
64
72
  process.exitCode = 1;
65
73
  return;
66
74
  }
67
75
 
68
76
  const skill = getEmbeddedSkill(name);
69
77
  if (!skill) {
70
- process.stderr.write(`error: unknown embedded skill "${name}" (${DEFAULT_SKC_DEFINITION_NAMES.join(", ")})\n`);
78
+ process.stderr.write(
79
+ `error: unknown embedded skill "${name}" (${DEFAULT_SKC_DEFINITION_NAMES.join(", ")}, or a bundled UI skill such as emil-design-eng)\n`,
80
+ );
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+
85
+ // Every embedded skill carries inline content; a missing body means the
86
+ // bundle was built wrong, so fail loudly instead of printing "undefined".
87
+ const content = skill.content;
88
+ if (typeof content !== "string" || content.length === 0) {
89
+ process.stderr.write(`error: embedded skill "${name}" has no bundled content\n`);
71
90
  process.exitCode = 1;
72
91
  return;
73
92
  }
@@ -77,12 +96,12 @@ export async function runSkillsCommand(cmd: SkillsCommandArgs): Promise<void> {
77
96
  description: skill.description,
78
97
  path: skill.filePath,
79
98
  source: skill.source,
80
- content: skill.content,
99
+ content,
81
100
  };
82
101
  if (cmd.flags?.json) {
83
102
  writeJson(entry);
84
103
  return;
85
104
  }
86
- process.stdout.write(skill.content);
87
- if (!skill.content.endsWith("\n")) process.stdout.write("\n");
105
+ process.stdout.write(content);
106
+ if (!content.endsWith("\n")) process.stdout.write("\n");
88
107
  }
@@ -15,6 +15,7 @@ const COMPONENTS: SetupComponent[] = [
15
15
  "provider",
16
16
  "python",
17
17
  "stt",
18
+ "ui-skills",
18
19
  ];
19
20
 
20
21
  export default class Setup extends Command {
@@ -58,9 +58,33 @@ function keptMalformedRecord(lockDir: string): GcRecord {
58
58
  };
59
59
  }
60
60
 
61
+ function emptyLockDirRecord(lockDir: string): GcRecord {
62
+ return {
63
+ store: "file_locks",
64
+ id: lockDir,
65
+ path: lockDir,
66
+ pid_status: "none",
67
+ status: "stale",
68
+ stale: true,
69
+ removable: true,
70
+ action: "none",
71
+ reason: "empty_file_lock_dir",
72
+ };
73
+ }
74
+
61
75
  async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
62
76
  const info = await readFileLockInfoForGc(lockDir);
63
- if (!info) return keptMalformedRecord(lockDir);
77
+ if (!info) {
78
+ // mkdir-before-info crash leftover: an empty `.lock` dir has no owner token
79
+ // and is safe to reclaim. Any other info-less shape stays fail-closed.
80
+ try {
81
+ const entries = await fs.readdir(lockDir);
82
+ if (entries.length === 0) return emptyLockDirRecord(lockDir);
83
+ } catch (error) {
84
+ if (!isEnoent(error)) throw error;
85
+ }
86
+ return keptMalformedRecord(lockDir);
87
+ }
64
88
 
65
89
  const probeResult = ctx.probe(info.pid);
66
90
  const pidStatus = gcPidStatusLabel(probeResult);
@@ -165,7 +189,20 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
165
189
  async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
166
190
  const lockDir = record.path ?? record.id;
167
191
  const info = await readFileLockInfoForGc(lockDir);
168
- if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
192
+ if (!info) {
193
+ if (record.reason !== "empty_file_lock_dir") {
194
+ return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
195
+ }
196
+ try {
197
+ const entries = await fs.readdir(lockDir);
198
+ if (entries.length !== 0) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
199
+ await fs.rmdir(lockDir);
200
+ return { removed: true };
201
+ } catch (error) {
202
+ if (isEnoent(error)) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
203
+ return { removed: false, error: errorMessage(error) };
204
+ }
205
+ }
169
206
 
170
207
  const probeResult = ctx.probe(info.pid);
171
208
  if (probeResult.status !== "dead") {
@@ -14,6 +14,21 @@ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
14
14
  retries: 50,
15
15
  retryDelayMs: 100,
16
16
  };
17
+ export class FileLockAcquireError extends Error {
18
+ readonly code = "acquire_timeout";
19
+
20
+ constructor(
21
+ readonly filePath: string,
22
+ readonly lockPath: string,
23
+ readonly attempts: number,
24
+ readonly holder: string,
25
+ ) {
26
+ super(
27
+ `Failed to acquire lock for ${filePath} after ${attempts} attempts: ${holder} (${lockPath}); a live owner is never displaced`,
28
+ );
29
+ this.name = "FileLockAcquireError";
30
+ }
31
+ }
17
32
 
18
33
  type LockInfo = FileLockOwnerToken;
19
34
 
@@ -243,6 +258,12 @@ async function releaseLock(lockPath: string, owner: FileLockOwnerToken): Promise
243
258
  const outcome = await removeFileLockDirForGc(lockPath, owner);
244
259
  if (outcome !== "removed") throw new Error(`Failed to release file lock: ${outcome}.`);
245
260
  }
261
+ async function lockHolderDescription(lockPath: string): Promise<string> {
262
+ const info = await readLockInfo(lockPath);
263
+ if (!info) return "unknown holder";
264
+ return `pid ${info.pid}`;
265
+ }
266
+
246
267
  async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
247
268
  const opts = { ...DEFAULT_OPTIONS, ...options };
248
269
  const lockPath = getLockPath(filePath);
@@ -255,7 +276,7 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
255
276
  if (await removeStaleLockForAcquire(lockPath, stale)) continue;
256
277
  await Bun.sleep(opts.retryDelayMs);
257
278
  }
258
- throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
279
+ throw new FileLockAcquireError(filePath, lockPath, opts.retries, await lockHolderDescription(lockPath));
259
280
  }
260
281
 
261
282
  /**
@@ -29,6 +29,7 @@ import {
29
29
  UNK_MAX_TOKENS,
30
30
  unregisterCustomApis,
31
31
  } from "@sayknow-cli/ai";
32
+ import { detectDiscoveredApiFamily } from "@sayknow-cli/ai/utils/discovery/openai-compatible";
32
33
 
33
34
  // Sentinel for local-only OAuth token (LM Studio, vLLM) — declared inline to avoid loading
34
35
  // any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
@@ -212,6 +213,36 @@ type ProviderValidationMode = "models-config" | "runtime-register";
212
213
 
213
214
  const OPENAI_REQUEST_TRANSFORM_APIS = new Set<Api>(["openai-completions", "openai-responses"]);
214
215
 
216
+ const OPENAI_FAMILY_APIS = new Set<Api>([
217
+ "openai-completions",
218
+ "openai-responses",
219
+ "openai-codex-responses",
220
+ "azure-openai-responses",
221
+ ]);
222
+
223
+ function isOpenAIFamilyApi(api: Api): boolean {
224
+ return OPENAI_FAMILY_APIS.has(api);
225
+ }
226
+
227
+ function dominantOpenAIFamilyModelApi(models: readonly { api?: string }[] | undefined): Api | undefined {
228
+ if (!models || models.length === 0) return undefined;
229
+ const counts = new Map<Api, number>();
230
+ for (const model of models) {
231
+ const api = model.api as Api | undefined;
232
+ if (api === undefined || !isOpenAIFamilyApi(api)) continue;
233
+ counts.set(api, (counts.get(api) ?? 0) + 1);
234
+ }
235
+ let best: Api | undefined;
236
+ let bestCount = 0;
237
+ for (const [api, count] of counts) {
238
+ if (count > bestCount) {
239
+ best = api;
240
+ bestCount = count;
241
+ }
242
+ }
243
+ return best;
244
+ }
245
+
215
246
  function getKnownProviderApis(providerName: string): Set<Api> {
216
247
  const apis = new Set<Api>();
217
248
  for (const model of getBundledModels(providerName as Parameters<typeof getBundledModels>[0])) {
@@ -1563,17 +1594,30 @@ export class ModelRegistry {
1563
1594
  keylessProviders.add(providerName);
1564
1595
  }
1565
1596
 
1566
- if (providerConfig.discovery && providerConfig.api) {
1597
+ const effectiveDiscoveryBaseUrl = providerConfig.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName);
1598
+ const providerApi =
1599
+ (providerConfig.api as Api | undefined) ??
1600
+ dominantOpenAIFamilyModelApi(providerConfig.models as { api?: string }[] | undefined);
1601
+ const autoDiscovery: ProviderDiscovery | undefined =
1602
+ !providerConfig.discovery &&
1603
+ !localOpenAICompat &&
1604
+ providerApi !== undefined &&
1605
+ isOpenAIFamilyApi(providerApi) &&
1606
+ effectiveDiscoveryBaseUrl !== undefined
1607
+ ? { type: "openai-models-list" }
1608
+ : undefined;
1609
+ const effectiveDiscovery = providerConfig.discovery ?? autoDiscovery;
1610
+ if (effectiveDiscovery && providerApi) {
1567
1611
  discoverableProviders.push({
1568
1612
  provider: providerName,
1569
- api: providerConfig.api as Api,
1570
- baseUrl: providerConfig.baseUrl ?? resolveProviderBaseUrlFromEnv(providerName),
1613
+ api: providerApi,
1614
+ baseUrl: effectiveDiscoveryBaseUrl,
1571
1615
  headers: providerConfig.headers,
1572
1616
  compat: providerConfig.compat,
1573
1617
  requestTransform: providerConfig.requestTransform,
1574
1618
  cacheRetention: providerConfig.cacheRetention,
1575
- discovery: providerConfig.discovery,
1576
- optional: false,
1619
+ discovery: effectiveDiscovery,
1620
+ optional: !providerConfig.discovery,
1577
1621
  });
1578
1622
  }
1579
1623
 
@@ -2280,50 +2324,82 @@ export class ModelRegistry {
2280
2324
  }
2281
2325
  return new Error(`HTTP ${response.status} from ${modelsUrl}`);
2282
2326
  },
2283
- mapModel: (item, defaults) => ({
2284
- ...defaults,
2285
- reasoning: isOmlx,
2286
- ...(isOmlx
2287
- ? {
2288
- thinking: {
2289
- mode: "effort" as const,
2290
- minLevel: Effort.Low,
2291
- maxLevel: Effort.High,
2292
- defaultLevel: Effort.Medium,
2293
- levels: [Effort.Low, Effort.Medium, Effort.High],
2294
- },
2295
- }
2296
- : {}),
2297
- contextWindow:
2298
- parseDiscoveryLimit(item.max_model_len) ??
2299
- parseDiscoveryLimit(item.context_length) ??
2300
- parseDiscoveryLimit(item.context_window) ??
2301
- parseDiscoveryLimit(item.max_context_length) ??
2302
- UNK_CONTEXT_WINDOW,
2303
- maxTokens:
2304
- parseDiscoveryLimit(item.max_completion_tokens) ??
2305
- parseDiscoveryLimit(item.max_tokens) ??
2306
- parseDiscoveryLimit(item.max_output_tokens) ??
2307
- UNK_MAX_TOKENS,
2308
- headers,
2309
- compat: {
2310
- ...(providerConfig.compat ?? {}),
2311
- supportsStore: false,
2312
- supportsDeveloperRole: false,
2313
- supportsReasoningEffort: isOmlx,
2327
+ mapModel: (item, defaults) => {
2328
+ const api = this.#resolveDiscoveredModelApi(
2329
+ providerConfig,
2330
+ typeof item.id === "string" ? item.id : "",
2331
+ item,
2332
+ );
2333
+ return {
2334
+ ...defaults,
2335
+ api,
2336
+ reasoning: isOmlx,
2314
2337
  ...(isOmlx
2315
2338
  ? {
2316
- thinkingFormat: "qwen-chat-template" as const,
2317
- reasoningContentField: "reasoning_content" as const,
2339
+ thinking: {
2340
+ mode: "effort" as const,
2341
+ minLevel: Effort.Low,
2342
+ maxLevel: Effort.High,
2343
+ defaultLevel: Effort.Medium,
2344
+ levels: [Effort.Low, Effort.Medium, Effort.High],
2345
+ },
2318
2346
  }
2319
2347
  : {}),
2320
- },
2321
- }),
2348
+ contextWindow:
2349
+ parseDiscoveryLimit(item.max_model_len) ??
2350
+ parseDiscoveryLimit(item.context_length) ??
2351
+ parseDiscoveryLimit(item.context_window) ??
2352
+ parseDiscoveryLimit(item.max_context_length) ??
2353
+ UNK_CONTEXT_WINDOW,
2354
+ maxTokens:
2355
+ parseDiscoveryLimit(item.max_completion_tokens) ??
2356
+ parseDiscoveryLimit(item.max_tokens) ??
2357
+ parseDiscoveryLimit(item.max_output_tokens) ??
2358
+ UNK_MAX_TOKENS,
2359
+ headers,
2360
+ compat: {
2361
+ ...(providerConfig.compat ?? {}),
2362
+ supportsStore: false,
2363
+ supportsDeveloperRole: false,
2364
+ supportsReasoningEffort: isOmlx,
2365
+ ...(isOmlx
2366
+ ? {
2367
+ thinkingFormat: "qwen-chat-template" as const,
2368
+ reasoningContentField: "reasoning_content" as const,
2369
+ }
2370
+ : {}),
2371
+ },
2372
+ };
2373
+ },
2322
2374
  });
2323
2375
  if (discovered === null) throw new Error(`Invalid OpenAI-compatible model catalog from ${baseUrl}`);
2324
2376
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
2325
2377
  }
2326
2378
 
2379
+ #resolveDiscoveredModelApi(
2380
+ providerConfig: DiscoveryProviderConfig,
2381
+ modelId: string,
2382
+ entry?: { id?: unknown; owned_by?: unknown },
2383
+ ): Api {
2384
+ let matchedPrefixLength = -1;
2385
+ let prefixApi: Api | undefined;
2386
+ for (const [prefix, routedApi] of Object.entries(providerConfig.discovery.apiByModelPrefix ?? {})) {
2387
+ if (modelId.startsWith(prefix) && prefix.length > matchedPrefixLength) {
2388
+ prefixApi = routedApi as Api;
2389
+ matchedPrefixLength = prefix.length;
2390
+ }
2391
+ }
2392
+ if (prefixApi !== undefined) return prefixApi;
2393
+ if (providerConfig.discovery.type === "openai-models-list") {
2394
+ const detected = detectDiscoveredApiFamily(entry ?? { id: modelId });
2395
+ if (detected === "anthropic-messages") return "anthropic-messages";
2396
+ if (detected === "openai-completions") {
2397
+ return isOpenAIFamilyApi(providerConfig.api) ? providerConfig.api : "openai-completions";
2398
+ }
2399
+ }
2400
+ return providerConfig.api;
2401
+ }
2402
+
2327
2403
  #normalizeLlamaCppBaseUrl(baseUrl?: string): string {
2328
2404
  const defaultBaseUrl = "http://127.0.0.1:8080";
2329
2405
  const raw = baseUrl || defaultBaseUrl;
@@ -185,6 +185,7 @@ export type ModelOverride = z.infer<typeof ModelOverrideSchema>;
185
185
 
186
186
  export const ProviderDiscoverySchema = z.object({
187
187
  type: z.enum(["ollama", "llama.cpp", "lm-studio", "omlx", "sglang", "openai-models-list"]),
188
+ apiByModelPrefix: z.record(z.string().min(1), z.string().min(1)).optional(),
188
189
  });
189
190
 
190
191
  const LocalOpenAICompatSchema = z
@@ -37,6 +37,9 @@ skc ultragoal complete-goals --retry-failed
37
37
  skc ultragoal checkpoint --goal-id <id> --status complete --evidence "<evidence>" --quality-gate-json <quality-gate-json-or-path>
38
38
  skc ultragoal checkpoint --goal-id <id> --status failed --evidence "<blocker/evidence>"
39
39
  skc ultragoal record-review-blockers --goal-id <id> --title "Resolve final review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>"
40
+ skc ultragoal succession offer --target-repo <path> --goal-id <id> --authorize "<statement>" --authorized-by <identity>
41
+ skc ultragoal succession adopt --offer <path>
42
+ skc ultragoal succession status --json
40
43
  ```
41
44
 
42
45
  Use these exact goal-tool calls for the inline goal state:
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Antmind Ventures Private Limited (appllama.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emil Kowalski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,43 @@
1
+ # Bundled frontend UI/UX skills
2
+
3
+ These `SKILL.md` files are vendored from upstream skill repositories so SKC
4
+ sessions can invoke frontend UI/UX craft skills without a separate user
5
+ install. They are **not** SKC public workflow skills: the four public
6
+ workflows remain `deep-interview`, `ralplan`, `ultragoal`, and `team`.
7
+
8
+ Regenerate with:
9
+
10
+ ```bash
11
+ bun scripts/build-ui-skill-bundle.ts \
12
+ --source emilkowalski=<checkout> \
13
+ --source appllama=<checkout>
14
+ ```
15
+
16
+ Only `SKILL.md` is compiled into the binary, and a bundled skill's `baseDir`
17
+ is a virtual `embedded:skc/...` path. Companion reference files are therefore
18
+ inlined into each `SKILL.md` as appendices by the generator; do not add
19
+ loose companion files here expecting them to be readable at runtime.
20
+
21
+ ## Sources
22
+
23
+ | Upstream | License | Vendored skills |
24
+ | --- | --- | --- |
25
+ | [emilkowalski/skills](https://github.com/emilkowalski/skills) | MIT (`LICENSE.emilkowalski`), Copyright (c) 2026 Emil Kowalski | `emil-design-eng`, `animate`, `review-animations`, `improve-animations`, `find-animation-opportunities`, `pick-ui-library`, `prototype`, `mobile-native`, `animation-vocabulary`, `apple-design`, `ask-sonner` |
26
+ | [Appllama/appllama-skills](https://github.com/Appllama/appllama-skills) | MIT (`LICENSE.appllama`) | `appllama-app-design-skill` |
27
+
28
+ ## Deliberate exclusions
29
+
30
+ - `write-swift`, `animate-expo` (emilkowalski) — not web frontend craft.
31
+ - `appllama-usage` (Appllama) — it exists only to drive the paid Appllama MCP
32
+ (`mcp.appllama.io`). SKC neither ships nor enables that connector, so
33
+ bundling it would instruct the agent to call tools that do not exist.
34
+ `appllama-app-design-skill` is upstream-documented as standalone.
35
+
36
+ - [`Jakubantalik/transitions.dev`](https://github.com/Jakubantalik/transitions.dev) — **not vendored.** Its terms license only the `transitions-dev` CLI and Refine tooling under MIT; the transition collection is governed by a separate "Using the transitions" clause that permits unlimited use in your own projects but forbids repackaging or publishing the collection (or a substantial part of it). SKC therefore never carries the content: the agent installs it into the user's own project on demand via `npx -y skills@latest add Jakubantalik/transitions.dev --skill transitions-dev -a universal -y`. See `hooks/ui-skill-keywords.ts`.
37
+ - [`DavidHDev/react-bits`](https://github.com/DavidHDev/react-bits) — **components not vendored.** React Bits is a shadcn component registry, not an agent skill repo (it ships no `SKILL.md`), and its MIT + Commons Clause license forbids selling, sublicensing, or redistributing the components "alone, in a bundle, or as a ported version". The bundled `react-bits` skill is therefore **SKC-authored guidance**: when to reach for the registry, how to install one component (`npx shadcn@latest add @react-bits/<Component>-<Variant>`), the `components.json` precondition, and a factual catalog of component names. No upstream component source is included; components are installed from the registry into the user's own project.
38
+
39
+ ## Trademarks
40
+
41
+ The Appllama name, llama, and logo are trademarks of Antmind Ventures Private
42
+ Limited. The MIT license does not grant rights to use them; they appear here
43
+ only to attribute the source of the vendored skill.