-
Notifications
You must be signed in to change notification settings - Fork 7
feat(skills): offer the agent skill install during onboarding #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
970dd4d
1a568be
6e5ce2e
64aa390
773416e
ec69f23
9bc8635
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@bunny.net/cli": minor | ||
| --- | ||
|
|
||
| feat(skills): `bunny login` offers a one-time global agent-skill install after authenticating (interactive runs only, skipped when already installed), and install.sh now points at `bunny skills install --global` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { existsSync, mkdirSync, writeFileSync } from "node:fs"; | ||
| import { dirname, join } from "node:path"; | ||
| import { loadConfigFile } from "../../config/index.ts"; | ||
| import { | ||
| installGlobalSkill, | ||
| isGlobalSkillInstalled, | ||
| isProjectSkillInstalled, | ||
| } from "../../core/agent-skill.ts"; | ||
| import { logger } from "../../core/logger.ts"; | ||
| import { confirmOrCancel, isInteractive } from "../../core/ui.ts"; | ||
| import { CACHE_DIR } from "../../core/update-check.ts"; | ||
| import { BUNNY_CLI_SKILL } from "./content.ts"; | ||
|
|
||
| // Losing the cache dir only means one repeat offer. | ||
| const OFFER_MARKER = join(CACHE_DIR, "skills-offered"); | ||
|
|
||
| const INSTALL_COMMAND = "bunny skills install --global"; | ||
|
|
||
| // Set once login has handled the offer (prompt or flag), so the post-command hint stays quiet in the same process. | ||
| let promptedThisRun = false; | ||
|
|
||
| function shouldOffer(output?: string): boolean { | ||
| return ( | ||
| isInteractive(output) && | ||
| !existsSync(OFFER_MARKER) && | ||
| !isGlobalSkillInstalled(BUNNY_CLI_SKILL.name) | ||
| ); | ||
| } | ||
|
|
||
| function markOffered(): void { | ||
| mkdirSync(dirname(OFFER_MARKER), { recursive: true }); | ||
| writeFileSync(OFFER_MARKER, `${new Date().toISOString()}\n`); | ||
| } | ||
|
|
||
| // Marks offered only on success, so a failed install is offered again on the next login. | ||
| function installAndReport(): void { | ||
| try { | ||
| installGlobalSkill(BUNNY_CLI_SKILL); | ||
| markOffered(); | ||
| logger.success( | ||
| "Agent skill installed to ~/.agents/skills and ~/.claude/skills.", | ||
| ); | ||
| } catch (err) { | ||
| logger.warn( | ||
| `Couldn't install the agent skill (${err instanceof Error ? err.message : err}); run: ${INSTALL_COMMAND}`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** One-time interactive offer to install the agent skill globally; never throws or blocks unattended runs. An explicit `installSkill` flag decides without prompting. */ | ||
| export async function offerGlobalSkillInstall( | ||
| output?: string, | ||
| installSkill?: boolean, | ||
| ): Promise<void> { | ||
| try { | ||
| if (installSkill === false) return; | ||
| if (installSkill === true) { | ||
| promptedThisRun = true; | ||
| installAndReport(); | ||
| return; | ||
| } | ||
| if (!shouldOffer(output)) return; | ||
| promptedThisRun = true; | ||
| logger.log(); | ||
| const answer = await confirmOrCancel( | ||
| "Install the bunny agent skill so AI coding tools (Claude Code, Cursor, Codex, ...) know how to use this CLI?", | ||
| { initial: true }, | ||
| ); | ||
| // An interrupted prompt is not an answer; the next login offers again. | ||
| if (answer === "cancel") return; | ||
| if (answer === "no") { | ||
| markOffered(); | ||
| logger.dim(`You can install it any time with: ${INSTALL_COMMAND}`); | ||
| return; | ||
| } | ||
| installAndReport(); | ||
| } catch {} | ||
| } | ||
|
|
||
| function hasCredentials(): boolean { | ||
| if (process.env.BUNNYNET_API_KEY) return true; | ||
| const file = loadConfigFile(); | ||
| return Object.keys(file?.profiles ?? {}).length > 0; | ||
| } | ||
|
|
||
| /** One-time passive stderr hint for users who authenticated without `bunny login`; never throws or prompts. */ | ||
| export function hintGlobalSkillInstall(): void { | ||
| try { | ||
| if ( | ||
| promptedThisRun || | ||
| !shouldOffer() || | ||
| !hasCredentials() || | ||
| isProjectSkillInstalled(process.cwd(), BUNNY_CLI_SKILL.name) | ||
| ) { | ||
| return; | ||
| } | ||
| markOffered(); | ||
| logger.dim( | ||
| `\nTip: run \`${INSTALL_COMMAND}\` so AI coding tools (Claude Code, Cursor, Codex, ...) know how to use this CLI.`, | ||
| ); | ||
| } catch {} | ||
|
greptile-apps[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the marker is written successfully but Useful? React with 👍 / 👎. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If writing a later skill file or the second global root fails after
SKILL.mdhas been written,installGlobalSkillleaves that partial output while skippingmarkOffered. The any-root installed check then suppresses subsequent offers, leaving the global skill incomplete instead of retrying the failed installation.