Skip to content

Commit 49e778f

Browse files
committed
fix(onboard): clarify keyed vs keyless skill setup prompts
Made-with: Cursor
1 parent 0544c6d commit 49e778f

2 files changed

Lines changed: 128 additions & 5 deletions

File tree

src/commands/onboard-skills.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,107 @@ describe("setupSkills", () => {
186186
const brewNote = notes.find((n) => n.title === "Homebrew recommended");
187187
expect(brewNote).toBeDefined();
188188
});
189+
190+
it("separates keyless dependency installs from keyed credential prompts", async () => {
191+
mocks.detectBinary.mockResolvedValue(true);
192+
mocks.installSkill.mockResolvedValue({
193+
ok: true,
194+
message: "Installed",
195+
stdout: "",
196+
stderr: "",
197+
code: 0,
198+
});
199+
mocks.buildWorkspaceSkillStatus.mockReturnValue({
200+
workspaceDir: "/tmp/ws",
201+
managedSkillsDir: "/tmp/managed",
202+
skills: [
203+
{
204+
...createBundledSkill({
205+
name: "openai-whisper",
206+
description: "Local Whisper transcription (no API key required)",
207+
bins: ["whisper"],
208+
installLabel: "Install whisper",
209+
}),
210+
skillKey: "openai-whisper",
211+
primaryEnv: undefined,
212+
missing: { bins: ["whisper"], anyBins: [], env: [], config: [], os: [] },
213+
},
214+
{
215+
...createBundledSkill({
216+
name: "openai-whisper-api",
217+
description: "Cloud Whisper API",
218+
bins: ["curl"],
219+
installLabel: "Install curl",
220+
}),
221+
skillKey: "openai-whisper-api",
222+
primaryEnv: "OPENAI_API_KEY",
223+
missing: {
224+
bins: ["curl"],
225+
anyBins: [],
226+
env: ["OPENAI_API_KEY"],
227+
config: [],
228+
os: [],
229+
},
230+
},
231+
{
232+
...createBundledSkill({
233+
name: "sag",
234+
description: "Speech generation",
235+
bins: ["ffmpeg"],
236+
installLabel: "Install ffmpeg",
237+
}),
238+
skillKey: "sag",
239+
primaryEnv: "ELEVENLABS_API_KEY",
240+
missing: {
241+
bins: ["ffmpeg"],
242+
anyBins: [],
243+
env: ["ELEVENLABS_API_KEY"],
244+
config: [],
245+
os: [],
246+
},
247+
},
248+
],
249+
} as never);
250+
251+
const notes: Array<{ title?: string; message: string }> = [];
252+
const confirmMessages: string[] = [];
253+
const promptTexts: string[] = [];
254+
const prompter: WizardPrompter = {
255+
intro: vi.fn(async () => {}),
256+
outro: vi.fn(async () => {}),
257+
note: vi.fn(async (message: string, title?: string) => {
258+
notes.push({ title, message });
259+
}),
260+
select: vi.fn(async () => "npm") as unknown as WizardPrompter["select"],
261+
multiselect: vi.fn(async () => ["__skip__"]) as unknown as WizardPrompter["multiselect"],
262+
text: vi.fn(async ({ message }: { message: string }) => {
263+
promptTexts.push(message);
264+
return "secret";
265+
}) as unknown as WizardPrompter["text"],
266+
confirm: vi.fn(async ({ message }: { message: string }) => {
267+
confirmMessages.push(message);
268+
if (message === "Configure skills now? (recommended)") {
269+
return true;
270+
}
271+
if (message.startsWith("Set OPENAI_API_KEY")) {
272+
return true;
273+
}
274+
return false;
275+
}) as unknown as WizardPrompter["confirm"],
276+
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
277+
};
278+
279+
await setupSkills({} as OpenClawConfig, "/tmp/ws", runtime, prompter);
280+
281+
const credentialNote = notes.find((entry) => entry.title === "Skill credentials");
282+
expect(credentialNote?.message).toContain("openai-whisper-api: OPENAI_API_KEY");
283+
expect(credentialNote?.message).toContain("sag: ELEVENLABS_API_KEY");
284+
expect(credentialNote?.message).not.toContain("openai-whisper:");
285+
286+
expect(confirmMessages).toContain("Set OPENAI_API_KEY for openai-whisper-api?");
287+
expect(confirmMessages).toContain("Set ELEVENLABS_API_KEY for sag?");
288+
expect(confirmMessages).not.toContain("Set OPENAI_API_KEY for openai-whisper?");
289+
expect(promptTexts).toContain("Enter OPENAI_API_KEY");
290+
expect(promptTexts).not.toContain("Enter ELEVENLABS_API_KEY");
291+
});
189292
});

src/commands/onboard-skills.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ function upsertSkillEntry(
4747
};
4848
}
4949

50+
function formatCredentialSetupMessage(skill: { name: string; primaryEnv: string }): string {
51+
return `Set ${skill.primaryEnv} for ${skill.name}?`;
52+
}
53+
5054
export async function setupSkills(
5155
cfg: OpenClawConfig,
5256
workspaceDir: string,
@@ -198,12 +202,28 @@ export async function setupSkills(
198202
}
199203
}
200204

201-
for (const skill of missing) {
202-
if (!skill.primaryEnv || skill.missing.env.length === 0) {
203-
continue;
204-
}
205+
const missingCredentialSkills = missing.filter(
206+
(skill) => Boolean(skill.primaryEnv) && skill.missing.env.length > 0,
207+
);
208+
if (missingCredentialSkills.length > 0) {
209+
await prompter.note(
210+
[
211+
"Credential setup is separate from dependency installation.",
212+
"You'll only be prompted for skills that explicitly require environment secrets:",
213+
...missingCredentialSkills.map(
214+
(skill) => `- ${skill.name}: ${skill.primaryEnv ?? skill.missing.env[0] ?? "secret"}`,
215+
),
216+
].join("\n"),
217+
"Skill credentials",
218+
);
219+
}
220+
221+
for (const skill of missingCredentialSkills) {
205222
const wantsKey = await prompter.confirm({
206-
message: `Set ${skill.primaryEnv} for ${skill.name}?`,
223+
message: formatCredentialSetupMessage({
224+
name: skill.name,
225+
primaryEnv: skill.primaryEnv as string,
226+
}),
207227
initialValue: false,
208228
});
209229
if (!wantsKey) {

0 commit comments

Comments
 (0)