Skip to content

Commit dd9e675

Browse files
fix(onboard): clarify skill credential prompts
1 parent 587b537 commit dd9e675

3 files changed

Lines changed: 86 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
2121

2222
### Fixes
2323

24+
- CLI/onboarding: separate skill API credential prompts from local dependency installs so keyless local skills such as OpenAI Whisper are not confused with API-backed skill setup. Fixes #74382. Thanks @sanjarcode.
2425
- Agents/errors: suppress malformed streaming tool-call JSON fragments before they reach chat surfaces while preserving provider request-validation diagnostics. Fixes #59076; keeps #59080 as duplicate coverage. (#59118) Thanks @singleGanghood.
2526
- CLI/models: restore provider-filtered `models list --all --provider <id>` rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd.
2627
- CLI/tools: keep the Gateway `tools.*` RPC namespace out of plugin command discovery and managed proxy startup, so stray commands like `openclaw tools effective` fail quickly instead of cold-loading plugin metadata. Refs #73477. Thanks @oromeis.

src/commands/onboard-skills.test.ts

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ import { setupSkills } from "./onboard-skills.js";
3131
function createBundledSkill(params: {
3232
name: string;
3333
description: string;
34-
bins: string[];
34+
bins?: string[];
35+
env?: string[];
36+
primaryEnv?: string;
3537
os?: string[];
36-
installLabel: string;
38+
installLabel?: string;
3739
}): {
3840
name: string;
3941
description: string;
@@ -42,6 +44,7 @@ function createBundledSkill(params: {
4244
filePath: string;
4345
baseDir: string;
4446
skillKey: string;
47+
primaryEnv?: string;
4548
always: boolean;
4649
disabled: boolean;
4750
blockedByAllowlist: boolean;
@@ -57,6 +60,8 @@ function createBundledSkill(params: {
5760
configChecks: [];
5861
install: Array<{ id: string; kind: string; label: string; bins: string[] }>;
5962
} {
63+
const bins = params.bins ?? [];
64+
const env = params.env ?? [];
6065
return {
6166
name: params.name,
6267
description: params.description,
@@ -65,14 +70,17 @@ function createBundledSkill(params: {
6570
filePath: `/tmp/skills/${params.name}`,
6671
baseDir: `/tmp/skills/${params.name}`,
6772
skillKey: params.name,
73+
primaryEnv: params.primaryEnv,
6874
always: false,
6975
disabled: false,
7076
blockedByAllowlist: false,
7177
eligible: false,
72-
requirements: { bins: params.bins, anyBins: [], env: [], config: [], os: params.os ?? [] },
73-
missing: { bins: params.bins, anyBins: [], env: [], config: [], os: params.os ?? [] },
78+
requirements: { bins, anyBins: [], env, config: [], os: params.os ?? [] },
79+
missing: { bins, anyBins: [], env, config: [], os: params.os ?? [] },
7480
configChecks: [],
75-
install: [{ id: "brew", kind: "brew", label: params.installLabel, bins: params.bins }],
81+
install: params.installLabel
82+
? [{ id: "brew", kind: "brew", label: params.installLabel, bins }]
83+
: [],
7684
};
7785
}
7886

@@ -186,4 +194,57 @@ describe("setupSkills", () => {
186194
const brewNote = notes.find((n) => n.title === "Homebrew recommended");
187195
expect(brewNote).toBeDefined();
188196
});
197+
198+
it("separates local dependency installs from API credential prompts", async () => {
199+
mockMissingBrewStatus([
200+
createBundledSkill({
201+
name: "openai-whisper",
202+
description: "Local speech-to-text with the Whisper CLI (no API key).",
203+
bins: ["whisper"],
204+
installLabel: "Install OpenAI Whisper (brew)",
205+
}),
206+
createBundledSkill({
207+
name: "openai-whisper-api",
208+
description: "Transcribe audio via OpenAI Audio Transcriptions API (Whisper).",
209+
env: ["OPENAI_API_KEY"],
210+
primaryEnv: "OPENAI_API_KEY",
211+
}),
212+
createBundledSkill({
213+
name: "sag",
214+
description: "ElevenLabs text-to-speech with mac-style say UX.",
215+
env: ["ELEVENLABS_API_KEY"],
216+
primaryEnv: "ELEVENLABS_API_KEY",
217+
}),
218+
]);
219+
220+
const { prompter, notes } = createPrompter({ multiselect: ["__skip__"] });
221+
await setupSkills({} as OpenClawConfig, "/tmp/ws", runtime, prompter);
222+
223+
const installCall = vi.mocked(prompter.multiselect).mock.calls[0]?.[0];
224+
expect(installCall?.message).toBe("Install missing skill dependencies");
225+
expect(installCall?.options).toEqual(
226+
expect.arrayContaining([
227+
expect.objectContaining({
228+
label: "🧩 openai-whisper",
229+
hint: expect.stringContaining("no API key"),
230+
}),
231+
]),
232+
);
233+
234+
const credentialNote = notes.find((n) => n.title === "Skill API credentials");
235+
expect(credentialNote?.message).toContain(
236+
"These prompts configure API-backed skills separately from local dependency installs.",
237+
);
238+
expect(credentialNote?.message).toContain("openai-whisper-api: OPENAI_API_KEY");
239+
expect(credentialNote?.message).toContain("sag: ELEVENLABS_API_KEY");
240+
241+
expect(vi.mocked(prompter.confirm)).toHaveBeenCalledWith({
242+
message: "Set OPENAI_API_KEY for openai-whisper-api?",
243+
initialValue: false,
244+
});
245+
expect(vi.mocked(prompter.confirm)).toHaveBeenCalledWith({
246+
message: "Set ELEVENLABS_API_KEY for sag?",
247+
initialValue: false,
248+
});
249+
});
189250
});

src/commands/onboard-skills.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ function formatSkillHint(skill: {
3030
return combined.length > maxLen ? `${combined.slice(0, maxLen - 1)}…` : combined;
3131
}
3232

33+
function formatCredentialSkillLine(skill: { name: string; primaryEnv: string }): string {
34+
return `- ${skill.name}: ${skill.primaryEnv}`;
35+
}
36+
3337
function upsertSkillEntry(
3438
cfg: OpenClawConfig,
3539
skillKey: string,
@@ -198,10 +202,21 @@ 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+
type CredentialSkill = (typeof missing)[number] & { primaryEnv: string };
206+
const credentialSkills = missing.filter(
207+
(skill): skill is CredentialSkill => Boolean(skill.primaryEnv) && skill.missing.env.length > 0,
208+
);
209+
if (credentialSkills.length > 0) {
210+
await prompter.note(
211+
[
212+
"These prompts configure API-backed skills separately from local dependency installs.",
213+
...credentialSkills.map(formatCredentialSkillLine),
214+
].join("\n"),
215+
"Skill API credentials",
216+
);
217+
}
218+
219+
for (const skill of credentialSkills) {
205220
const wantsKey = await prompter.confirm({
206221
message: `Set ${skill.primaryEnv} for ${skill.name}?`,
207222
initialValue: false,

0 commit comments

Comments
 (0)