/** * `skills sync` — the last mile: write skills from this machine's corpus * (the migrated owner-layout cache ~/.hasna/skills/skills//, or the * legacy ~/.hasna/skills/installed// when not migrated) into each coding * agent's global skills directory, per-tool adapted, so an agent auto-loads * them. * * This is the deliberate reversal of the old "pins, not installs" stub: agent skill * folders used to be left entirely unmanaged and every write path returned success:false. * They are now written — but only the ones this tool owns. Ownership is tracked with a * marker file. An unmarked directory is left alone by default; only one that already has * SKILL.md can be explicitly adopted, while any other unmarked directory is never touched. */ import { renameSync } from "node:fs"; /** * The coding agents `skills sync` targets by default. These runtimes load a * `/./skills//SKILL.md` tree (OpenCode is the one path exception). * Gemini is retired; Windsurf/pi are addressable through installSkillForAgent but are * not in the default fan-out. */ export type SyncAgent = "claude" | "codewith" | "codex" | "opencode" | "cursor"; export declare const SYNC_AGENTS: readonly SyncAgent[]; /** * Environment variable naming an explicit canonical-corpus source for sync. The CLI * `--source ` flag takes precedence; this is the ambient form, and it means the * same thing: the npm package ships no corpus, so a machine that has not pulled from an * instance (or that is not a checkout of the monorepo) must point sync at where the * corpus actually is. */ export declare const SKILLS_SOURCE_ENV = "SKILLS_SOURCE"; /** * Ownership marker written beside every SKILL.md this tool syncs. Its exact managedBy * value tells a re-sync which directories this tool owns; a foreign or malformed * marker grants no authority. A hidden sidecar rather than a frontmatter field so the SKILL.md the * agent loads stays exactly the adapted document and nothing else. */ export declare const SYNC_MARKER_FILE = ".hasna-skills.json"; export declare const SYNC_MARKER_MANAGED_BY = "@hasna/skills"; export interface SyncMarker { managedBy: string; skill: string; source: string; syncedAt: string; } /** Internal shared ownership predicate; not exported by the public package entrypoint. */ export declare function isSkillsOwnershipMarker(marker: unknown): marker is Record; /** Read only a regular ownership sidecar. */ export declare function hasSkillsOwnershipMarker(dir: string): boolean; export declare function isSyncAgent(value: string): value is SyncAgent; export declare function resolveSyncAgents(arg?: string): SyncAgent[]; /** The global skills directory for an agent, honouring a test-supplied home. */ export declare function agentGlobalSkillsDir(agent: SyncAgent, homeDir?: string): string; /** * Adapt a SKILL.md for a target agent. * * `user_invocable` is a Claude-only frontmatter field (it controls Claude's slash menu). * The Claude copy carries it; every other agent's copy has it stripped, because the field * is meaningless there and skill-sync policy is that non-Claude copies must not carry * Claude-specific frontmatter. The body is left verbatim: automatically rewriting prose * would corrupt meaning, and corpus/instruction skills are authored tool-neutral. */ export declare function adaptSkillMdForAgent(skillMd: string, agent: string): string; /** * The sentence every sync pointer stub carries in its body. It survives per-agent * adaptation (adaptSkillMdForAgent only touches `user_invocable`), so it is the stable * fingerprint for "this document is a pointer, not the skill's real content". */ export declare const POINTER_MARKER_PHRASE = "This is an executable skill from the @hasna/skills catalog"; /** * True when a SKILL.md is a sync pointer stub: frontmatter declares `kind: executable` * AND the body carries the canonical pointer sentence. A full content document is never * a stub even when it mentions the sentence, because it lacks the kind marker. */ export declare function isPointerSkillMd(markdown: string): boolean; /** A pointer SKILL.md for an executable skill: what it is and how to actually run it. */ export declare function pointerSkillMd(name: string, description: string): string; export type SyncActionKind = "create" | "update" | "skip"; export interface AgentSyncAction { skill: string; agent: SyncAgent; path: string; action: SyncActionKind; reason?: string; } export interface SyncSkillsOptions { /** Specific skills to sync. When empty, every corpus skill is synced. */ names?: string[]; /** Explicit; the default when no names are given is already "all". */ all?: boolean; /** Target agents. Defaults to SYNC_AGENTS. */ agents?: SyncAgent[]; /** Report intended actions without writing anything. */ dryRun?: boolean; /** Adopt an unmanaged agent skill only when its directory already contains SKILL.md. */ force?: boolean; /** Corpus root override. Tests only. */ rootDir?: string; /** * Explicit canonical-corpus source: a directory of skill folders, or the monorepo * package root (which contains `skills/`). Takes precedence over * $SKILLS_SOURCE, which takes precedence over the installed corpus cache. * Agent-workflow skills are NOT part of the public repo corpus anymore — they * moved to the private per-station store (fleet-resources) and * reach sync through the installed cache — so a package root resolves to * `skills/` only. */ sourceDir?: string; /** Home directory override for agent skill dirs. Tests only. */ homeDir?: string; } export interface SyncSkillsResult { actions: AgentSyncAction[]; } /** * Resolve where sync reads the canonical corpus from. * * The package ships no bundled corpus. Precedence is explicit-over-ambient: * 1. `options.sourceDir` - an explicit source: a corpus dir, or a package root * containing `skills/` * 2. `$SKILLS_SOURCE` - the ambient spelling of the same thing * 3. the installed cache - `getPortableSkillsRoot()` (what `skills pull` writes), * resolved through `resolveCorpusRoot` so a migrated owner layout * (~/.hasna/skills/skills/) is read in preference to `installed/`. This cache is * also where private agent-workflow skills arrive from the per-station store * (fleet-resources) and get synced into agent folders. * * A missing explicit source is an error, not a fallback to "nothing": the whole point * of zero-corpus is that sync must not silently sync an empty corpus because the * package directory changed shape. */ export declare function resolveSyncCorpus(options?: SyncSkillsOptions): { roots: string[]; source: "source" | "corpus"; }; export declare function syncSkillsToAgents(options?: SyncSkillsOptions): SyncSkillsResult; export interface WriteManagedAgentSkillParams { skill: string; agent: SyncAgent; skillMd: string; source?: "source" | "corpus"; resourceDir?: string; homeDir?: string; dryRun?: boolean; force?: boolean; } /** * Write one skill into one agent's global folder, non-clobbering. * * A directory this tool has written before carries our exact ownership marker and is replaced with * an exact mirror — except that a managed home holding full content is never silently * replaced with an executable pointer stub (that would be data loss; it is refused * unless `force` is passed). A directory with a SKILL.md but no valid Skills marker is * skipped unless `force` explicitly adopts it. Any other pre-existing unmanaged * directory is always left untouched. A fresh directory is created. */ export declare function writeManagedAgentSkill(params: WriteManagedAgentSkillParams): AgentSyncAction; export interface ManagedDirWriteResult { action: SyncActionKind; path: string; reason?: string; } export interface ManagedDirWriteOptions { skill: string; source?: string; resourceDir?: string; dryRun?: boolean; force?: boolean; /** Test seam for exercising rollback after the original directory has moved. */ renameDirectory?: typeof renameSync; } /** * The primitive both the fan-out sync and the single-skill installer share: replace one * owned skill directory with an exact staged mirror, non-clobbering, and stamp it as ours. * `dir` is the skill's own directory (…/skills/), so callers control scope (global * vs project) by choosing the directory. */ export declare function writeManagedSkillDir(dir: string, skillMd: string, options: ManagedDirWriteOptions): ManagedDirWriteResult; /** * Remove a skill this tool synced from an agent folder. Refuses to delete a directory it * did not write (no valid Skills ownership marker). Foreign or malformed markers * grant no deletion authority, and removal has no force override. */ export declare function removeManagedAgentSkill(skill: string, agent: SyncAgent, homeDir?: string): boolean;