-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmultica-sync-skills
More file actions
executable file
·224 lines (196 loc) · 6.05 KB
/
multica-sync-skills
File metadata and controls
executable file
·224 lines (196 loc) · 6.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env zsh
usage="NAME
multica-sync-skills - sync skills from a local directory into a Multica workspace
SYNOPSIS
multica-sync-skills [-h] [-d DIR] [-n] [-v]
DESCRIPTION
Reads SKILL.md files from a directory (one subdirectory per skill) and
creates or updates skills in the active Multica workspace. Only updates a
skill when its content or description has changed. No AI is involved - this
is a pure file-diff-and-CLI-call operation.
Each skill directory must contain a SKILL.md file with YAML frontmatter:
---
name: skill-name
description: Short description shown to agents
---
... skill body ...
ARGUMENTS
-d, --dir DIR
Directory containing skill subdirectories (default: ~/.claude/skills)
-n, --dry-run
Show what would be created/updated without making any changes.
-v, --verbose
Print a line for each skill that is already up to date.
-h, --help
Show this help message.
EXAMPLES
multica-sync-skills
multica-sync-skills -d ~/code/dotfiles-private/dot_claude/skills
multica-sync-skills -d ~/code/dotfiles-private/dot_claude/skills --dry-run
"
skills_dir="${HOME}/.claude/skills"
dry_run=0
verbose=0
while (( $# > 0 )); do
case "$1" in
-h | --help)
echo "$usage"
exit 0
;;
-d | --dir)
skills_dir=$2
shift 2
;;
-n | --dry-run)
dry_run=1
shift
;;
-v | --verbose)
verbose=1
shift
;;
-*)
echo "Unknown option: $1" >&2
echo "$usage" >&2
exit 1
;;
*)
break
;;
esac
done
if [[ ! -d "$skills_dir" ]]; then
echo "Error: skills directory not found: $skills_dir" >&2
exit 1
fi
# Load existing Multica skills into an associative array keyed by name
typeset -A existing_ids existing_descriptions existing_contents
existing_json=$(multica skill list --output json 2>/dev/null)
if [[ $? -ne 0 ]]; then
echo "Error: failed to list skills from Multica workspace" >&2
exit 1
fi
while IFS= read -r line; do
id=$(echo "$line" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null)
name=$(echo "$line" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('name',''))" 2>/dev/null)
desc=$(echo "$line" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('description',''))" 2>/dev/null)
content=$(echo "$line" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('content',''))" 2>/dev/null)
if [[ -n "$name" && -n "$id" ]]; then
existing_ids[$name]=$id
existing_descriptions[$name]=$desc
existing_contents[$name]=$content
fi
done < <(echo "$existing_json" | python3 -c "
import json, sys
skills = json.load(sys.stdin)
for s in skills:
print(json.dumps(s))
" 2>/dev/null)
created=0
updated=0
skipped=0
errors=0
for skill_dir in "$skills_dir"/*/; do
skill_file="${skill_dir}SKILL.md"
[[ -f "$skill_file" ]] || continue
# Parse frontmatter (between first pair of --- lines)
name=$(python3 - "$skill_file" <<'PYEOF'
import sys, re
path = sys.argv[1]
with open(path) as f:
content = f.read()
m = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if not m:
sys.exit(0)
for line in m.group(1).splitlines():
if line.startswith('name:'):
print(line.split(':', 1)[1].strip())
break
PYEOF
)
description=$(python3 - "$skill_file" <<'PYEOF'
import sys, re
path = sys.argv[1]
with open(path) as f:
content = f.read()
m = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if not m:
sys.exit(0)
for line in m.group(1).splitlines():
if line.startswith('description:'):
print(line.split(':', 1)[1].strip())
break
PYEOF
)
if [[ -z "$name" ]]; then
echo "Warning: no 'name' in frontmatter of $skill_file, skipping" >&2
continue
fi
# Full file content is the skill body (everything after frontmatter)
body=$(python3 - "$skill_file" <<'PYEOF'
import sys, re
path = sys.argv[1]
with open(path) as f:
content = f.read()
m = re.match(r'^---\s*\n.*?\n---\s*\n', content, re.DOTALL)
if m:
print(content[m.end():], end='')
else:
print(content, end='')
PYEOF
)
if [[ -n "${existing_ids[$name]}" ]]; then
# Skill exists - check if update needed
skill_id=${existing_ids[$name]}
remote_desc=${existing_descriptions[$name]}
remote_content=${existing_contents[$name]}
if [[ "$description" == "$remote_desc" && "$body" == "$remote_content" ]]; then
(( verbose )) && echo " up-to-date $name"
(( skipped++ ))
continue
fi
if (( dry_run )); then
echo " would update $name"
(( updated++ ))
continue
fi
result=$(multica skill update "$skill_id" \
--name "$name" \
--description "$description" \
--content "$body" \
--output json 2>&1)
if [[ $? -eq 0 ]]; then
echo " updated $name"
(( updated++ ))
else
echo " error updating $name: $result" >&2
(( errors++ ))
fi
else
# Skill does not exist - create it
if (( dry_run )); then
echo " would create $name"
(( created++ ))
continue
fi
result=$(multica skill create \
--name "$name" \
--description "$description" \
--content "$body" \
--output json 2>&1)
if [[ $? -eq 0 ]]; then
echo " created $name"
(( created++ ))
else
echo " error creating $name: $result" >&2
(( errors++ ))
fi
fi
done
echo ""
if (( dry_run )); then
echo "Dry run complete: $created would be created, $updated would be updated, $skipped already up to date"
else
echo "Done: $created created, $updated updated, $skipped already up to date, $errors errors"
fi
(( errors > 0 )) && exit 1 || exit 0