feat: import local skill folders and zip bundles#52
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37035c9b6c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0870f7fb9c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: df9dfa4b78
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b00250611d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c8cc522b4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 194c336f81
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb2181ce58
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b958684115
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c1cf15efa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a5a5949fe8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a8c0840ecc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cdd8168723
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Keep both PR #52's local skill import feature (schemas, client methods, tests) and main's cloud runtime node + grouped issues additions. Deduplicate EMPTY_CREATE_AGENT_FROM_TEMPLATE_RESPONSE import; adopt main's hast-util-to-html version (^9.0.5) while keeping PR's jszip dep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Stop reading folder files past the per-skill byte budget (MAX_SKILL_ACCEPTED_BYTES) so large folders don't freeze the browser - Report "unreadable_skill_md" instead of "missing_skill_md" when SKILL.md exists but is binary or oversized (folder + zip paths) - Reject hidden (.dot) and metadata (Thumbs.db, desktop.ini) file paths server-side in normalizeLocalUploadedSkillFilePath - Add i18n for the new unreadable_skill_md error message (en + zh-Hans) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: acec3a103f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
acec3a1 to
15e36be
Compare
15e36be to
aa2e0cf
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa2e0cfb3b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (path !== joinPath(root, "SKILL.md") && !isHiddenPath(path) && !isMetadataFile(path)) { | ||
| const count = supportingFileCountsByRoot.get(root) ?? 0; | ||
| if (count >= MAX_SKILL_FILES) { | ||
| entries.push({ path, skippedReason: "too_many_files" }); | ||
| continue; | ||
| } | ||
| supportingFileCountsByRoot.set(root, count + 1); | ||
| } |
There was a problem hiding this comment.
Count zip supporting files only after text validation
In readZipFile, the per-root supporting-file counter is incremented before the file content is decoded and validated, but binary/non-UTF-8 files are only rejected later in buildLocalSkillCandidates. This means skipped binary files still consume the 128-file budget for zip uploads; for example, a zip with 128 binary files followed by a valid template will mark the valid template as too_many_files, even though folder uploads (and the stated skip policy) do not count rejected binaries. Defer the count increment until after a file is confirmed importable text.
Useful? React with 👍 / 👎.
| } | ||
| supportingFileCountsByRoot.set(root, count + 1); | ||
| } | ||
| const blob = await entry.async("blob"); |
There was a problem hiding this comment.
Enforce bundle byte budget before inflating zip entries
This loop inflates each selected zip entry with entry.async("blob") without applying the 8 MiB per-skill bundle cap first. A valid-looking archive with 128 entries near 1 MiB each can therefore allocate roughly 128 MiB in-browser before the later candidate-building step skips most files as bundle_too_large, which can cause significant UI stalls or memory pressure. Apply a per-root byte budget check before blob inflation (or keep excess entries as skipped metadata) so zip uploads are bounded like folder uploads.
Useful? React with 👍 / 👎.
| const blob = await entry.async("blob"); | ||
| entries.push({ | ||
| path, | ||
| file: new File([blob], basename(path)), |
There was a problem hiding this comment.
Skip hidden zip entries before inflating blob contents
readZipFile still materializes blob data for hidden/metadata entries under a detected skill root, even though those files are always dropped later by buildLocalSkillCandidates. In archives containing many large dotfiles (for example .git-style paths), this eagerly allocates substantial browser memory for files that can never be imported, which can slow or freeze preview. Short-circuit hidden/metadata entries before entry.async("blob") and record them as skipped instead of inflating them.
Useful? React with 👍 / 👎.
Add a fourth creation method to the Add Skill dialog: "Upload folder or zip". Users can select local skill directories via folder picker, zip files, or drag-and-drop, preview detected skills with editable names and descriptions, and import up to 16 skills per batch. Browser-side parsing (packages/views/skills/utils/local-skill-upload.ts): - Path normalization rejecting absolute, traversal, hidden, and metadata paths - SKILL.md discovery and multi-skill grouping by nearest skill root - UTF-8 validation with fatal TextDecoder, null byte detection - Per-file (1 MiB), per-skill (128 files, 8 MiB), per-batch (16 skills) limits - Zip entry pre-inflate size check to avoid decompression bombs - Zip entries scoped to detected skill roots only Upload panel (packages/views/skills/components/local-skill-upload-panel.tsx): - Dashed drop zone with folder picker and zip picker buttons - Single-skill and multi-skill preview with editable fields - Batch selection with dynamic enable/disable at the 16-skill cap - Import progress and structured result summary (created/skipped/failed) - File input value reset for re-selection of same path - Full en + zh-Hans localization Backend API (server/internal/handler/skill_import_local.go): - POST /api/skills/import-local with per-item structured results - Server-side validation: path rules, per-file and bundle size caps, hidden/metadata file rejection, null byte sanitization - MaxBytesReader at 32 MiB to bound JSON decode memory - Partial success: duplicate names reported as skipped, not failed API client (packages/core/api/): - importLocalSkills method with Zod response schema and parseWithFallback - Malformed response graceful degradation to empty result Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
aa2e0cf to
fe14574
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe14574965
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (parts.includes("..")) { | ||
| return { ok: false, reason: "path_traversal" }; | ||
| } |
There was a problem hiding this comment.
Reject dot path segments during upload normalization
normalizeUploadPath rejects .. but still accepts . segments, so entries like skill/./templates/a.md are treated as valid in preview and sent to the API. The backend path validator (normalizeLocalUploadedSkillFilePath) rejects . segments and returns invalid_file_path, causing imports that looked valid in the UI to fail at submit time. This is reproducible with zips produced by tools that preserve ./ path components; align client normalization with the server by rejecting or normalizing . segments before candidate creation.
Useful? React with 👍 / 👎.
| if strings.TrimSpace(skill.Content) == "" { | ||
| return "missing_skill_md" |
There was a problem hiding this comment.
Accept empty SKILL.md content for local imports
This endpoint rejects any imported skill whose content is empty or whitespace-only (missing_skill_md), but SKILL.md presence is already established by the upload parser and regular skill creation allows empty content. As a result, a valid bundle containing an intentionally blank SKILL.md passes preview and then fails only at import time, creating inconsistent behavior between local import and POST /api/skills.
Useful? React with 👍 / 👎.
| const [summary, setSummary] = useState<ImportSummary | null>(null); | ||
|
|
||
| const selectedCandidates = useMemo( | ||
| () => candidates.filter((candidate) => candidate.valid && candidate.selected && candidate.name.trim()), |
There was a problem hiding this comment.
Enforce batch cap independently of editable names
The selected-count gate is derived from selectedCandidates, which excludes entries whose name.trim() is empty. In multi-skill uploads, a user can temporarily blank one selected name to drop the counted total below 16, select an additional skill, then restore the blanked name and submit more than 16 skills; the request then fails with the server's too many skills error even though the UI was supposed to enforce the cap. Count selected valid candidates separately from name validity when enforcing the checkbox/import limit.
Useful? React with 👍 / 👎.
| const skillRoots = normalizedInputs | ||
| .filter((input) => | ||
| basename(input.path) === "SKILL.md" && | ||
| (input.skippedReason !== undefined || | ||
| (!!input.file && input.file.size <= MAX_SKILL_FILE_BYTES)), | ||
| ) | ||
| .map((input) => dirname(input.path)) | ||
| .sort() | ||
| .filter((root, index, roots) => !hasAncestorSkillRoot(root, roots.slice(0, index))); |
There was a problem hiding this comment.
Keep child skill roots when parent SKILL.md is unreadable
Skill-root discovery currently treats any SKILL.md (including one already marked unreadable/oversized) as an ancestor root, so a bad top-level SKILL.md suppresses all nested valid roots via hasAncestorSkillRoot. In a bundle containing an unreadable root SKILL.md plus valid subdir/SKILL.md skills, only the invalid top-level candidate is returned and the valid child skills become unimportable. Ancestor filtering should ignore invalid parent roots or defer parent-child collapsing until the parent SKILL.md is confirmed readable.
Useful? React with 👍 / 👎.
Summary
Test Plan
cd server && go test ./internal/handler -run 'TestImportLocalSkills|TestNormalizeLocalUploadedSkillFilePathRejectsBackslashTraversal|TestRuntimeLocalSkillImportFlow_EndToEnd' -count=1pnpm --filter @multica/core test -- api/client.test.tspnpm --filter @multica/views test -- skills/utils/local-skill-upload.test.ts skills/components/local-skill-upload-panel.test.tsx skills/components/create-skill-dialog.test.tsx locales/parity.test.tspnpm --filter @multica/views typecheckpnpm --filter @multica/core typecheckgit diff --check origin/main...HEAD