Add install-from-prompt to the Skills tab

Skill marketplaces (SkillsMP and friends) publish an install prompt meant to
be pasted into an interactive claude, which fetches the skill's files and
places them under a skills directory. Handler has no interactive claude and
its skills are DB rows, so the Skills tab gains an "Install from a marketplace
prompt" card wired to a new skill_install command: the worker runs the pasted
prompt through a one-off headless claude in a throwaway staging directory
(sandboxed by a generated settings.json allowing fetch/clone tooling with
acceptEdits), then imports whatever <skill>/SKILL.md landed as managed rows —
reinstalling a skill updates it in place.

Headless means nobody can answer questions mid-install, so the wrapper prompt
front-loads the answers a human would give: install into the staging dir,
always user scope (Handler distributes skills to workers itself), pick the
instructions' defaults, never stop to ask, and end with a report of the
choices made — surfaced in the command result for after-the-fact review, with
the imported skill editable/disableable in the UI.

Multi-file skills survive the import: a new claude_skill_files table
(migration 0011, alongside the command-type constraint change) captures
auxiliary files (references/, scripts/, ...), the launch-time sync rebuilds
each managed skill dir from them, and skill cards list what a skill ships
with. The one-off run's timeout defaults under worker_stale_after so a slow
install can't get the worker's live runs falsely reaped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f42XmjtVsc3zQ9Dhn6DqZ
This commit is contained in:
Claude
2026-07-23 15:06:22 +00:00
parent 07d8c3aa19
commit 6a14823c26
41 changed files with 731 additions and 55 deletions
+52
View File
@@ -137,6 +137,8 @@ interface StoreValue {
createClaudeSkill: (b: SkillBody) => Promise<boolean>;
updateClaudeSkill: (id: number, b: Partial<SkillBody>) => Promise<boolean>;
deleteClaudeSkill: (id: number) => Promise<void>;
/* Run a pasted marketplace install prompt on the worker and import the result. */
installClaudeSkill: (prompt: string) => Promise<boolean>;
createClaudeConnector: (b: ConnectorBody) => Promise<boolean>;
updateClaudeConnector: (id: number, b: Partial<ConnectorBody>) => Promise<boolean>;
deleteClaudeConnector: (id: number) => Promise<void>;
@@ -1061,6 +1063,55 @@ export function DashboardProvider({
[claudeWrite],
);
const installClaudeSkill = useCallback(
async (prompt: string): Promise<boolean> => {
// A worker-run command (the API has no claude), tracked like login/spawn. The
// worker-side run is budgeted at ~4 minutes; poll a little past that.
setCmd({
text: "skill install: running the marketplace prompt through headless claude on the worker…",
error: false,
busy: true,
});
try {
const command = await clientRef.current.api<Command>("/claude/skills/install", {
method: "POST",
body: { prompt },
});
const final = await clientRef.current.trackCommand(command.id, { attempts: 600 });
if (!final) {
setCmd({
text: "skill install: still running (see Activity). Is the control worker running?",
error: false,
busy: false,
});
return false;
}
if (final.status !== "done") {
setCmd({
text: `skill install failed — ${final.error ?? "unknown error"}`,
error: true,
busy: false,
});
return false;
}
const skills = (final.result?.skills ?? []) as { name: string; action: string }[];
const names = skills.map((s) => `${s.name} (${s.action})`).join(", ");
setCmd({
text: `skill install done: ${names || "no skills"} — review the import below; Claude's report of the choices it made is in Activity.`,
error: false,
busy: false,
});
await loadClaude();
return true;
} catch (e) {
if (e instanceof AuthError) return false;
setCmd({ text: `skill install failed: ${(e as Error).message}`, error: true, busy: false });
return false;
}
},
[loadClaude],
);
const createClaudeConnector = useCallback(
(b: ConnectorBody) =>
claudeWrite(
@@ -1210,6 +1261,7 @@ export function DashboardProvider({
createClaudeSkill,
updateClaudeSkill,
deleteClaudeSkill,
installClaudeSkill,
createClaudeConnector,
updateClaudeConnector,
deleteClaudeConnector,