@miphamai/cli 0.48.0 → 0.49.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.
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
2
+ import { join, dirname } from 'node:path'
3
+ import { homedir } from 'node:os'
4
+ import { BUNDLED_SKILL_ASSETS, type BundledSkillAsset } from './bundled-skill-assets'
5
+
6
+ export interface SkillAssetsOptions {
7
+ /** Base dir under which assets land as `<baseDir>/<skillName>/...`. Defaults to `~/.mipham/skills`. */
8
+ baseDir?: string
9
+ /** Asset map. Defaults to the compiled-in snapshot. Injectable for tests. */
10
+ assets?: Record<string, BundledSkillAsset[]>
11
+ }
12
+
13
+ /**
14
+ * Idempotently extract a skill's bundled executable assets to disk.
15
+ * Content-compare: only writes when a file is missing or its content drifted,
16
+ * so user-added files (e.g. site-patterns/*.md) are never overwritten.
17
+ * Assets are UTF-8 text only today — binary assets would need base64 (see
18
+ * generate-bundled-skills.ts). Returns the extraction root, or null if the
19
+ * skill bundles no assets.
20
+ */
21
+ export function ensureSkillAssets(skillName: string, opts?: SkillAssetsOptions): string | null {
22
+ const map = opts?.assets ?? BUNDLED_SKILL_ASSETS
23
+ const base = opts?.baseDir ?? join(homedir(), '.mipham', 'skills')
24
+ const list = map[skillName]
25
+ if (!list) return null
26
+ const root = join(base, skillName)
27
+ for (const a of list) {
28
+ const dest = join(root, a.path)
29
+ const needsWrite = !existsSync(dest) || readFileSync(dest, 'utf-8') !== a.content
30
+ if (needsWrite) {
31
+ mkdirSync(dirname(dest), { recursive: true })
32
+ // Preserve the source exec bit (scripts/*.mjs), default 0o644 for docs.
33
+ writeFileSync(dest, a.content, { mode: a.mode ?? 0o644 })
34
+ }
35
+ }
36
+ return root
37
+ }
@@ -1,6 +1,7 @@
1
- import type { ToolDefinition } from '../../shared/index.ts'
1
+ import type { ToolDefinition } from '../../shared'
2
2
  import { executeForkedSkill } from '../../skills/fork-executor'
3
- import { sanitizeSkillBody } from '../../skills/sanitizer.js'
3
+ import { sanitizeSkillBody } from '../../skills/sanitizer'
4
+ import { ensureSkillAssets } from '../../skills/skill-assets'
4
5
 
5
6
  export const skillTool: ToolDefinition = {
6
7
  name: 'Skill',
@@ -36,6 +37,16 @@ export const skillTool: ToolDefinition = {
36
37
  }
37
38
  }
38
39
 
40
+ // Extract executable assets (scripts/references) if this skill bundles them.
41
+ // No-op for every skill that has no entry in BUNDLED_SKILL_ASSETS.
42
+ // A write failure (EACCES/ENOSPC) must not abort invocation: the skill body
43
+ // still delivers its own recovery guidance for the missing-script case.
44
+ try {
45
+ ensureSkillAssets(skillName)
46
+ } catch (err) {
47
+ console.warn(`Skill asset extraction failed for "${skillName}":`, err)
48
+ }
49
+
39
50
  // Check if skill has context: fork — execute in isolated subagent
40
51
  if (skill.context === 'fork') {
41
52
  const registry = ctx.registry