From f2c1c2aa1755dfcd36a20216c52f4bf809e7e5f4 Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:09:05 -0400 Subject: [PATCH 01/11] Sync Rewind into the public skills catalog --- .changeset/calm-clips-rewind.md | 6 ++ .github/workflows/sync-public-skills.yml | 5 +- packages/core/src/cli/skills.spec.ts | 33 +++++----- packages/core/src/cli/skills.ts | 5 ++ packages/skills/src/index.spec.ts | 78 ++++++++++++++++++++++++ packages/skills/src/index.ts | 8 ++- 6 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 .changeset/calm-clips-rewind.md diff --git a/.changeset/calm-clips-rewind.md b/.changeset/calm-clips-rewind.md new file mode 100644 index 0000000000..1f1f777a7d --- /dev/null +++ b/.changeset/calm-clips-rewind.md @@ -0,0 +1,6 @@ +--- +"@agent-native/skills": patch +"@agent-native/core": patch +--- + +Install Rewind through Agent Native's complete local Screen Memory setup instead of treating it as a plain public skill, and reject disconnected installs that disable its required MCP connection. diff --git a/.github/workflows/sync-public-skills.yml b/.github/workflows/sync-public-skills.yml index 848f5451ce..537177a9b6 100644 --- a/.github/workflows/sync-public-skills.yml +++ b/.github/workflows/sync-public-skills.yml @@ -10,6 +10,7 @@ on: - "templates/plan/.agents/skills/visual-plan/**" - "templates/plan/.agents/skills/visual-recap/**" - "packages/core/src/cli/skills.ts" + - "packages/core/src/cli/skills-content/rewind-skill.ts" - ".github/workflows/sync-public-skills.yml" workflow_dispatch: @@ -21,7 +22,7 @@ concurrency: jobs: dispatch: - name: Ask BuilderIO/skills to sync visual skills + name: Ask BuilderIO/skills to sync exported skills runs-on: ubuntu-latest steps: - name: Generate app token @@ -39,7 +40,7 @@ jobs: run: | gh api repos/BuilderIO/skills/dispatches \ --method POST \ - -f event_type=agent-native-plan-skills-updated \ + -f event_type=agent-native-public-skills-updated \ -f "client_payload[sourceRepository]=${{ github.repository }}" \ -f "client_payload[sourceSha]=${{ github.sha }}" \ -f "client_payload[sourceRunUrl]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/packages/core/src/cli/skills.spec.ts b/packages/core/src/cli/skills.spec.ts index f5514a416d..52e2a169ff 100644 --- a/packages/core/src/cli/skills.spec.ts +++ b/packages/core/src/cli/skills.spec.ts @@ -191,7 +191,7 @@ describe("agent-native skills", () => { } }); - it("can install only the Rewind instructions without touching MCP config", async () => { + it("rejects Rewind installs that disable the local Screen Memory MCP", async () => { const root = tmpDir(); const codexHome = path.join(root, "codex-home"); fs.mkdirSync(codexHome, { recursive: true }); @@ -199,24 +199,25 @@ describe("agent-native skills", () => { process.env.CODEX_HOME = codexHome; try { - const result = await addAgentNativeSkill( - parseSkillsArgs([ - "add", - "rewind", - "--client", - "codex", - "--scope", - "user", - "--no-mcp", - "--yes", - ]), - { baseDir: root }, - ); + await expect( + addAgentNativeSkill( + parseSkillsArgs([ + "add", + "rewind", + "--client", + "codex", + "--scope", + "user", + "--no-mcp", + "--yes", + ]), + { baseDir: root }, + ), + ).rejects.toThrow("cannot be installed with --no-mcp"); - expect(result.mcpClients).toEqual([]); expect( fs.existsSync(path.join(codexHome, "skills", "rewind", "SKILL.md")), - ).toBe(true); + ).toBe(false); expect(fs.existsSync(path.join(codexHome, "config.toml"))).toBe(false); } finally { if (previousCodexHome === undefined) delete process.env.CODEX_HOME; diff --git a/packages/core/src/cli/skills.ts b/packages/core/src/cli/skills.ts index 70542ac177..0f4ace678a 100644 --- a/packages/core/src/cli/skills.ts +++ b/packages/core/src/cli/skills.ts @@ -3376,6 +3376,11 @@ export async function addAgentNativeSkill( const knownBuiltIn = knownTarget ? BUILT_IN_APP_SKILLS[knownTarget] : null; const installsScreenMemoryMcp = isScreenMemoryMcpBuiltInSkill(knownBuiltIn); const baseDir = options.baseDir ?? process.cwd(); + if (installsScreenMemoryMcp && !parsed.mcp) { + throw new Error( + "Rewind requires the local Clips Screen Memory MCP and cannot be installed with --no-mcp.", + ); + } if (installsScreenMemoryMcp && parsed.mcpUrl) { throw new Error( "Rewind uses the local Clips Screen Memory MCP and does not accept --mcp-url.", diff --git a/packages/skills/src/index.spec.ts b/packages/skills/src/index.spec.ts index 8f119144f0..c9c6a3cf9f 100644 --- a/packages/skills/src/index.spec.ts +++ b/packages/skills/src/index.spec.ts @@ -746,6 +746,84 @@ describe("@agent-native/skills", () => { ); }); + it("delegates Rewind installs to the core local Screen Memory flow", async () => { + const project = tmpDir(); + const previousDirect = process.env.AGENT_NATIVE_SKILLS_DIRECT; + delete process.env.AGENT_NATIVE_SKILLS_DIRECT; + + try { + await runSkillsCli( + [ + "add", + "--skill", + "rewind", + "--client", + "codex", + "--scope", + "user", + "--yes", + "--json", + ], + { baseDir: project, isInteractive: () => false }, + ); + } finally { + if (previousDirect === undefined) + delete process.env.AGENT_NATIVE_SKILLS_DIRECT; + else process.env.AGENT_NATIVE_SKILLS_DIRECT = previousDirect; + } + + expect(runCoreSkills).toHaveBeenCalledWith( + [ + "add", + "rewind", + "--client", + "codex", + "--scope", + "user", + "--yes", + "--json", + ], + expect.objectContaining({ + baseDir: project, + catalogMode: "all", + isInteractive: expect.any(Function), + publicSkillEntries: [], + publicSkillSource: "BuilderIO/skills", + }), + ); + }); + + it("surfaces Core's rejection when Rewind MCP setup is disabled", async () => { + const project = tmpDir(); + vi.mocked(runCoreSkills).mockRejectedValueOnce( + new Error( + "Rewind requires the local Clips Screen Memory MCP and cannot be installed with --no-mcp.", + ), + ); + + await expect( + runSkillsCli( + [ + "add", + "--skill", + "rewind", + "--client", + "codex", + "--scope", + "user", + "--no-mcp", + "--yes", + ], + { baseDir: project, isInteractive: () => false }, + ), + ).rejects.toThrow("cannot be installed with --no-mcp"); + + expect(runCoreSkills).toHaveBeenCalledWith( + expect.arrayContaining(["rewind", "--no-mcp"]), + expect.objectContaining({ baseDir: project }), + ); + }); + it("delegates content local-files installs to agent-native core", async () => { const project = tmpDir(); const previousDirect = process.env.AGENT_NATIVE_SKILLS_DIRECT; diff --git a/packages/skills/src/index.ts b/packages/skills/src/index.ts index 6a9d505e62..1cbec30ebe 100644 --- a/packages/skills/src/index.ts +++ b/packages/skills/src/index.ts @@ -385,7 +385,13 @@ function shouldLoadPublicCatalog(parsed: ParsedArgs): boolean { if (parsed.command !== "add") return false; if (parsed.copySource && parsed.source) return true; if (parsed.skillNames.length === 0) return true; - return parsed.skillNames.some((name) => !resolveAppForSkill(name)); + return parsed.skillNames.some((name) => !isCoreDelegatedSkill(name)); +} + +function isCoreDelegatedSkill(skillName: string): boolean { + // Rewind uses Core's local Screen Memory installer, not the standalone + // package's hosted MCP descriptor path. + return skillName === "rewind" || Boolean(resolveAppForSkill(skillName)); } const HIDDEN_STANDALONE_BUILT_INS = [ From 6cab953a421d02eac686f93b8417cc87be56b9c1 Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Thu, 23 Jul 2026 07:49:12 -0400 Subject: [PATCH 02/11] Guide Rewind users through Clips setup --- .changeset/calm-clips-rewind.md | 2 +- .../src/cli/skills-content/rewind-skill.ts | 40 +++++++- packages/core/src/cli/skills.spec.ts | 91 ++++++++++++++++++- packages/core/src/cli/skills.ts | 2 +- 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/.changeset/calm-clips-rewind.md b/.changeset/calm-clips-rewind.md index 1f1f777a7d..55280ed2dd 100644 --- a/.changeset/calm-clips-rewind.md +++ b/.changeset/calm-clips-rewind.md @@ -3,4 +3,4 @@ "@agent-native/core": patch --- -Install Rewind through Agent Native's complete local Screen Memory setup instead of treating it as a plain public skill, and reject disconnected installs that disable its required MCP connection. +Install Rewind through Agent Native's complete local Screen Memory setup instead of treating it as a plain public skill, reject disconnected installs that disable its required MCP connection, and guide first-time users through consent-gated Clips Desktop setup. diff --git a/packages/core/src/cli/skills-content/rewind-skill.ts b/packages/core/src/cli/skills-content/rewind-skill.ts index 3bde26f64c..2c030feeee 100644 --- a/packages/core/src/cli/skills-content/rewind-skill.ts +++ b/packages/core/src/cli/skills-content/rewind-skill.ts @@ -13,6 +13,38 @@ metadata: Use Clips Rewind as local screen memory. Start broad enough to find the right moment, then read only the smallest relevant range. +## First-Run Setup + +Call \`screen_memory_status\` before assuming Rewind is ready. If the tool is +unavailable, do not stop at a missing-tool error and do not silently install +anything: + +1. Explain that Rewind requires the signed Clips Desktop app on macOS plus a + local agent connection. +2. Ask whether Clips is installed, or use a non-mutating native application + lookup when the host supports one. Do not inspect Clips' app-data folders. + If Clips is absent or its presence cannot be confirmed, ask permission + before opening or downloading anything: “Clips Desktop is required for + Rewind. Would you like me to open the official installer and guide you + through setup?” +3. Only after affirmative permission, open + \`https://clips.agent-native.com/download\` in the user's browser. Complete + native installation steps only when the current host can safely do so and + the user explicitly permitted them. Never bypass Gatekeeper, accept macOS + permission prompts, or enable screen/audio capture on the user's behalf. +4. If native UI control is unavailable, return the download link and wait for + the user to confirm that Clips is installed and running. +5. Ask the user to enable Rewind in the Clips tray and choose the intended + capture mode. Then configure the local connection with: + + \`npx -y @agent-native/core@latest skills add rewind --client --scope user --yes\` + + Replace \`\` with the current compatible host: \`codex\`, + \`claude-code\`, \`cursor\`, \`opencode\`, \`github-copilot\`, or \`cowork\`. +6. Restart the host only if it cannot reload MCP servers in place, then retry + \`screen_memory_status\`. Do not claim setup succeeded until it reports + Rewind enabled and unpaused. + ## Retrieval Flow 1. Call \`screen_memory_status\` first. If the newest segment is still open, @@ -41,12 +73,12 @@ moment, then read only the smallest relevant range. bypass the Screen Memory MCP broker. - Do not upload frames returned by local Screen Memory tools. - Treat foreground apps and chapter labels as evidence, not proof of intent. -- If the Screen Memory MCP is missing, explain that the one-time setup needs to - be repaired with: +- If Clips is installed and Rewind is enabled but the Screen Memory MCP is + missing, explain that only the agent connection needs repair with: \`npx -y @agent-native/core@latest skills add rewind --client --scope user --yes\` - Replace \`\` with the current compatible host: \`codex\`, - \`claude-code\`, \`cursor\`, \`opencode\`, \`github-copilot\`, or \`cowork\`. Ask the user to restart the host if it cannot reload MCP servers in place. +- Never conflate installing Clips Desktop with installing the skill/MCP + connection, and never claim either succeeded without direct evidence. `; diff --git a/packages/core/src/cli/skills.spec.ts b/packages/core/src/cli/skills.spec.ts index 52e2a169ff..77f1bc3489 100644 --- a/packages/core/src/cli/skills.spec.ts +++ b/packages/core/src/cli/skills.spec.ts @@ -170,7 +170,13 @@ describe("agent-native skills", () => { path.join(codexHome, "skills", "rewind", "SKILL.md"), "utf-8", ), - ).toContain("screen_memory_search_chapters"); + ).toContain("ask permission\n before opening or downloading anything"); + expect( + fs.readFileSync( + path.join(codexHome, "skills", "rewind", "SKILL.md"), + "utf-8", + ), + ).toContain("https://clips.agent-native.com/download"); const config = fs.readFileSync( path.join(codexHome, "config.toml"), "utf-8", @@ -225,6 +231,46 @@ describe("agent-native skills", () => { } }); + it("directs missing-store installs to Clips without claiming native setup", async () => { + const root = tmpDir(); + const home = path.join(root, "home"); + fs.mkdirSync(home, { recursive: true }); + const previousHome = process.env.HOME; + const previousStore = process.env.CLIPS_SCREEN_MEMORY_DIR; + const previousLegacyStore = process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; + process.env.HOME = home; + delete process.env.CLIPS_SCREEN_MEMORY_DIR; + delete process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; + + try { + await expect( + addAgentNativeSkill( + parseSkillsArgs([ + "add", + "rewind", + "--client", + "codex", + "--scope", + "user", + "--yes", + ]), + { baseDir: root }, + ), + ).rejects.toThrow( + /https:\/\/clips\.agent-native\.com\/download.*was not installed or enabled automatically/, + ); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + if (previousStore === undefined) + delete process.env.CLIPS_SCREEN_MEMORY_DIR; + else process.env.CLIPS_SCREEN_MEMORY_DIR = previousStore; + if (previousLegacyStore === undefined) + delete process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; + else process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR = previousLegacyStore; + } + }); + it("installs Rewind into the shared user skill directory for Cursor", async () => { const root = tmpDir(); const home = path.join(root, "home"); @@ -268,6 +314,49 @@ describe("agent-native skills", () => { } }); + it("installs Rewind instructions and local MCP config for Claude Code", async () => { + const root = tmpDir(); + const home = path.join(root, "home"); + const store = path.join(root, "screen-memory"); + fs.mkdirSync(home, { recursive: true }); + fs.mkdirSync(store, { recursive: true }); + const previousHome = process.env.HOME; + const previousStore = process.env.CLIPS_SCREEN_MEMORY_DIR; + process.env.HOME = home; + process.env.CLIPS_SCREEN_MEMORY_DIR = store; + + try { + const result = await addAgentNativeSkill( + parseSkillsArgs([ + "add", + "rewind", + "--client", + "claude-code", + "--scope", + "user", + "--yes", + ]), + { baseDir: root }, + ); + + expect(result.mcpClients).toEqual(["claude-code"]); + expect( + fs.existsSync( + path.join(home, ".claude", "skills", "rewind", "SKILL.md"), + ), + ).toBe(true); + expect( + fs.readFileSync(path.join(home, ".claude.json"), "utf-8"), + ).toContain("clips-screen-memory"); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + if (previousStore === undefined) + delete process.env.CLIPS_SCREEN_MEMORY_DIR; + else process.env.CLIPS_SCREEN_MEMORY_DIR = previousStore; + } + }); + it("dry-runs Rewind setup without requiring an active local store", async () => { const result = await addAgentNativeSkill( parseSkillsArgs([ diff --git a/packages/core/src/cli/skills.ts b/packages/core/src/cli/skills.ts index 0f4ace678a..cfe423f68d 100644 --- a/packages/core/src/cli/skills.ts +++ b/packages/core/src/cli/skills.ts @@ -3629,7 +3629,7 @@ export async function addAgentNativeSkill( const screenMemoryDir = resolveScreenMemoryStoreDir(); if (!screenMemoryDir) { throw new Error( - "No local Clips Screen Memory store was found. Turn Rewind on in Clips, then run the setup again.", + "No local Clips Screen Memory store was found. Clips Desktop is required for Rewind. Download and launch the signed app from https://clips.agent-native.com/download, turn Rewind on, then run the setup again. Clips Desktop was not installed or enabled automatically.", ); } registeredMcpClients = mcpClients.map((client) => { From 706ef114d4e9658e04b8e45e217861342982364e Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:00:00 -0400 Subject: [PATCH 03/11] Preflight Rewind before writing skill files --- packages/core/src/cli/skills.spec.ts | 11 +++++++++++ packages/core/src/cli/skills.ts | 18 +++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/core/src/cli/skills.spec.ts b/packages/core/src/cli/skills.spec.ts index 77f1bc3489..b845adda9d 100644 --- a/packages/core/src/cli/skills.spec.ts +++ b/packages/core/src/cli/skills.spec.ts @@ -234,11 +234,15 @@ describe("agent-native skills", () => { it("directs missing-store installs to Clips without claiming native setup", async () => { const root = tmpDir(); const home = path.join(root, "home"); + const codexHome = path.join(root, "codex-home"); fs.mkdirSync(home, { recursive: true }); + fs.mkdirSync(codexHome, { recursive: true }); const previousHome = process.env.HOME; + const previousCodexHome = process.env.CODEX_HOME; const previousStore = process.env.CLIPS_SCREEN_MEMORY_DIR; const previousLegacyStore = process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; process.env.HOME = home; + process.env.CODEX_HOME = codexHome; delete process.env.CLIPS_SCREEN_MEMORY_DIR; delete process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; @@ -259,9 +263,16 @@ describe("agent-native skills", () => { ).rejects.toThrow( /https:\/\/clips\.agent-native\.com\/download.*was not installed or enabled automatically/, ); + + expect( + fs.existsSync(path.join(codexHome, "skills", "rewind", "SKILL.md")), + ).toBe(false); + expect(fs.existsSync(path.join(codexHome, "config.toml"))).toBe(false); } finally { if (previousHome === undefined) delete process.env.HOME; else process.env.HOME = previousHome; + if (previousCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousCodexHome; if (previousStore === undefined) delete process.env.CLIPS_SCREEN_MEMORY_DIR; else process.env.CLIPS_SCREEN_MEMORY_DIR = previousStore; diff --git a/packages/core/src/cli/skills.ts b/packages/core/src/cli/skills.ts index cfe423f68d..0babed1ef2 100644 --- a/packages/core/src/cli/skills.ts +++ b/packages/core/src/cli/skills.ts @@ -3549,8 +3549,18 @@ export async function addAgentNativeSkill( let connectCommand: string | undefined; let registeredMcpClients: ClientId[] = shouldRegisterMcp ? mcpClients : []; let localManifestPath: string | undefined; + const screenMemoryDir = + shouldRegisterMcp && installsScreenMemoryMcp + ? resolveScreenMemoryStoreDir() + : undefined; try { + if (shouldRegisterMcp && installsScreenMemoryMcp && !screenMemoryDir) { + throw new Error( + "No local Clips Screen Memory store was found. Clips Desktop is required for Rewind. Download and launch the signed app from https://clips.agent-native.com/download, turn Rewind on, then run the setup again. Clips Desktop was not installed or enabled automatically.", + ); + } + if (parsed.instructions) { if (skillsAgents.length === 0) { if (!shouldRegisterMcp) { @@ -3626,19 +3636,13 @@ export async function addAgentNativeSkill( }); if (shouldRegisterMcp && installsScreenMemoryMcp) { - const screenMemoryDir = resolveScreenMemoryStoreDir(); - if (!screenMemoryDir) { - throw new Error( - "No local Clips Screen Memory store was found. Clips Desktop is required for Rewind. Download and launch the signed app from https://clips.agent-native.com/download, turn Rewind on, then run the setup again. Clips Desktop was not installed or enabled automatically.", - ); - } registeredMcpClients = mcpClients.map((client) => { commands.push( `npx @agent-native/core@latest mcp install-screen-memory --client ${client} --scope ${parsed.scope}`, ); installScreenMemoryForClient( client, - screenMemoryDir, + screenMemoryDir!, baseDir, parsed.scope, ); From 82c5542f5d6f5a704c032d2ffc66466fa41f1ef3 Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:10:38 -0400 Subject: [PATCH 04/11] Keep legacy skill sync dispatch during transition --- .github/workflows/sync-public-skills.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/sync-public-skills.yml b/.github/workflows/sync-public-skills.yml index 537177a9b6..a5636c088b 100644 --- a/.github/workflows/sync-public-skills.yml +++ b/.github/workflows/sync-public-skills.yml @@ -44,3 +44,10 @@ jobs: -f "client_payload[sourceRepository]=${{ github.repository }}" \ -f "client_payload[sourceSha]=${{ github.sha }}" \ -f "client_payload[sourceRunUrl]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + gh api repos/BuilderIO/skills/dispatches \ + --method POST \ + -f event_type=agent-native-plan-skills-updated \ + -f "client_payload[sourceRepository]=${{ github.repository }}" \ + -f "client_payload[sourceSha]=${{ github.sha }}" \ + -f "client_payload[sourceRunUrl]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" From 57e81e745d4dc79f66dd965739d5dacea7087397 Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:11:42 -0400 Subject: [PATCH 05/11] Prioritize legacy skill sync dispatch --- .github/workflows/sync-public-skills.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-public-skills.yml b/.github/workflows/sync-public-skills.yml index a5636c088b..ed45fc968f 100644 --- a/.github/workflows/sync-public-skills.yml +++ b/.github/workflows/sync-public-skills.yml @@ -40,14 +40,14 @@ jobs: run: | gh api repos/BuilderIO/skills/dispatches \ --method POST \ - -f event_type=agent-native-public-skills-updated \ + -f event_type=agent-native-plan-skills-updated \ -f "client_payload[sourceRepository]=${{ github.repository }}" \ -f "client_payload[sourceSha]=${{ github.sha }}" \ -f "client_payload[sourceRunUrl]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" gh api repos/BuilderIO/skills/dispatches \ --method POST \ - -f event_type=agent-native-plan-skills-updated \ + -f event_type=agent-native-public-skills-updated \ -f "client_payload[sourceRepository]=${{ github.repository }}" \ -f "client_payload[sourceSha]=${{ github.sha }}" \ -f "client_payload[sourceRunUrl]=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" From 7531abd915fa6e883344f2133d3d4b0ca8662c06 Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:17:25 -0400 Subject: [PATCH 06/11] fix(cli): reject Rewind before local writes --- packages/core/src/cli/skills.spec.ts | 47 ++++++++++++++++++++++++++++ packages/core/src/cli/skills.ts | 32 +++++++++++++------ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/core/src/cli/skills.spec.ts b/packages/core/src/cli/skills.spec.ts index b845adda9d..4ba8a91244 100644 --- a/packages/core/src/cli/skills.spec.ts +++ b/packages/core/src/cli/skills.spec.ts @@ -282,6 +282,53 @@ describe("agent-native skills", () => { } }); + it("rejects a missing Rewind store before telemetry or user config writes", async () => { + const root = tmpDir(); + const home = path.join(root, "home"); + const codexHome = path.join(root, "codex-home"); + fs.mkdirSync(home, { recursive: true }); + fs.mkdirSync(codexHome, { recursive: true }); + const previousHome = process.env.HOME; + const previousCodexHome = process.env.CODEX_HOME; + const previousStore = process.env.CLIPS_SCREEN_MEMORY_DIR; + const previousLegacyStore = process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; + process.env.HOME = home; + process.env.CODEX_HOME = codexHome; + delete process.env.CLIPS_SCREEN_MEMORY_DIR; + delete process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; + const telemetry = { + track: vi.fn(), + flush: vi.fn(async () => undefined), + }; + + try { + await expect( + runSkills( + ["add", "rewind", "--client", "codex", "--scope", "user", "--yes"], + { baseDir: root, telemetry }, + ), + ).rejects.toThrow( + /https:\/\/clips\.agent-native\.com\/download.*was not installed or enabled automatically/, + ); + + expect(telemetry.track).not.toHaveBeenCalled(); + expect(telemetry.flush).not.toHaveBeenCalled(); + expect(fs.readdirSync(home)).toEqual([]); + expect(fs.readdirSync(codexHome)).toEqual([]); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + if (previousCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousCodexHome; + if (previousStore === undefined) + delete process.env.CLIPS_SCREEN_MEMORY_DIR; + else process.env.CLIPS_SCREEN_MEMORY_DIR = previousStore; + if (previousLegacyStore === undefined) + delete process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; + else process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR = previousLegacyStore; + } + }); + it("installs Rewind into the shared user skill directory for Cursor", async () => { const root = tmpDir(); const home = path.join(root, "home"); diff --git a/packages/core/src/cli/skills.ts b/packages/core/src/cli/skills.ts index 0babed1ef2..5999ea3856 100644 --- a/packages/core/src/cli/skills.ts +++ b/packages/core/src/cli/skills.ts @@ -850,6 +850,26 @@ function isKnownSkill(value: string | undefined): boolean { return Boolean(normalizeKnownSkillTarget(value)); } +const REWIND_MISSING_STORE_ERROR = + "No local Clips Screen Memory store was found. Clips Desktop is required for Rewind. Download and launch the signed app from https://clips.agent-native.com/download, turn Rewind on, then run the setup again. Clips Desktop was not installed or enabled automatically."; + +function preflightRewindStore(parsed: ParsedSkillsArgs): string | undefined { + if ( + parsed.command !== "add" || + parsed.dryRun || + !parsed.mcp || + parsed.mcpUrl + ) { + return undefined; + } + const knownTarget = normalizeKnownSkillTarget(parsed.target ?? "assets"); + const knownBuiltIn = knownTarget ? BUILT_IN_APP_SKILLS[knownTarget] : null; + if (!isScreenMemoryMcpBuiltInSkill(knownBuiltIn)) return undefined; + const screenMemoryDir = resolveScreenMemoryStoreDir(); + if (!screenMemoryDir) throw new Error(REWIND_MISSING_STORE_ERROR); + return screenMemoryDir; +} + function isLocalOnlyBuiltInSkill( entry: (typeof BUILT_IN_APP_SKILLS)[BuiltInAppSkillId] | null | undefined, ): boolean { @@ -3542,6 +3562,7 @@ export async function addAgentNativeSkill( } } const commands: string[] = []; + const screenMemoryDir = preflightRewindStore(parsed); const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "an-skills-add-")); let instructionSource: string | undefined; let instructionsWritten: string[] | undefined; @@ -3549,18 +3570,8 @@ export async function addAgentNativeSkill( let connectCommand: string | undefined; let registeredMcpClients: ClientId[] = shouldRegisterMcp ? mcpClients : []; let localManifestPath: string | undefined; - const screenMemoryDir = - shouldRegisterMcp && installsScreenMemoryMcp - ? resolveScreenMemoryStoreDir() - : undefined; try { - if (shouldRegisterMcp && installsScreenMemoryMcp && !screenMemoryDir) { - throw new Error( - "No local Clips Screen Memory store was found. Clips Desktop is required for Rewind. Download and launch the signed app from https://clips.agent-native.com/download, turn Rewind on, then run the setup again. Clips Desktop was not installed or enabled automatically.", - ); - } - if (parsed.instructions) { if (skillsAgents.length === 0) { if (!shouldRegisterMcp) { @@ -4063,6 +4074,7 @@ export async function runSkills( if (parsed.baseDir) { options = { ...options, baseDir: path.resolve(parsed.baseDir) }; } + preflightRewindStore(parsed); const clackForLog = parsed.printJson ? undefined : await import("@clack/prompts"); From 33f710b7a15bc6b20607a3f8fbd779fa16e713ef Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:22:01 -0400 Subject: [PATCH 07/11] fix(cli): preflight Rewind skill flags --- packages/core/src/cli/skills.spec.ts | 17 +++++++++++++++++ packages/core/src/cli/skills.ts | 9 ++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/core/src/cli/skills.spec.ts b/packages/core/src/cli/skills.spec.ts index 4ba8a91244..83f9dc5332 100644 --- a/packages/core/src/cli/skills.spec.ts +++ b/packages/core/src/cli/skills.spec.ts @@ -310,6 +310,23 @@ describe("agent-native skills", () => { ).rejects.toThrow( /https:\/\/clips\.agent-native\.com\/download.*was not installed or enabled automatically/, ); + await expect( + runSkills( + [ + "add", + "--skill", + "rewind", + "--client", + "codex", + "--scope", + "user", + "--yes", + ], + { baseDir: root, telemetry }, + ), + ).rejects.toThrow( + /https:\/\/clips\.agent-native\.com\/download.*was not installed or enabled automatically/, + ); expect(telemetry.track).not.toHaveBeenCalled(); expect(telemetry.flush).not.toHaveBeenCalled(); diff --git a/packages/core/src/cli/skills.ts b/packages/core/src/cli/skills.ts index 5999ea3856..8832f16585 100644 --- a/packages/core/src/cli/skills.ts +++ b/packages/core/src/cli/skills.ts @@ -862,9 +862,12 @@ function preflightRewindStore(parsed: ParsedSkillsArgs): string | undefined { ) { return undefined; } - const knownTarget = normalizeKnownSkillTarget(parsed.target ?? "assets"); - const knownBuiltIn = knownTarget ? BUILT_IN_APP_SKILLS[knownTarget] : null; - if (!isScreenMemoryMcpBuiltInSkill(knownBuiltIn)) return undefined; + const explicitlyTargetsRewind = + normalizeKnownSkillTarget(parsed.target ?? "assets") === "rewind" || + parsed.plainSkillNames?.some( + (skillName) => normalizeKnownSkillTarget(skillName) === "rewind", + ); + if (!explicitlyTargetsRewind) return undefined; const screenMemoryDir = resolveScreenMemoryStoreDir(); if (!screenMemoryDir) throw new Error(REWIND_MISSING_STORE_ERROR); return screenMemoryDir; From 456514bf44e5b40fcc785d0aff065b02c2f5be88 Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:32:07 -0400 Subject: [PATCH 08/11] test: preserve rewind preflight telemetry invariant --- packages/core/src/cli/skills.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/cli/skills.spec.ts b/packages/core/src/cli/skills.spec.ts index 83f9dc5332..07639f82dc 100644 --- a/packages/core/src/cli/skills.spec.ts +++ b/packages/core/src/cli/skills.spec.ts @@ -298,6 +298,7 @@ describe("agent-native skills", () => { delete process.env.AGENT_NATIVE_SCREEN_MEMORY_DIR; const telemetry = { track: vi.fn(), + captureException: vi.fn(), flush: vi.fn(async () => undefined), }; @@ -329,6 +330,7 @@ describe("agent-native skills", () => { ); expect(telemetry.track).not.toHaveBeenCalled(); + expect(telemetry.captureException).not.toHaveBeenCalled(); expect(telemetry.flush).not.toHaveBeenCalled(); expect(fs.readdirSync(home)).toEqual([]); expect(fs.readdirSync(codexHome)).toEqual([]); From 5ccceac543a661c6158da0e6d13357421478ed3d Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:38:12 -0400 Subject: [PATCH 09/11] fix: defer cli identity writes until telemetry events --- packages/core/src/cli/telemetry.ts | 6 +++--- packages/skills/src/telemetry.spec.ts | 20 ++++++++++++++++++++ packages/skills/src/telemetry.ts | 6 +++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/core/src/cli/telemetry.ts b/packages/core/src/cli/telemetry.ts index ff4d35f139..66a034c71b 100644 --- a/packages/core/src/cli/telemetry.ts +++ b/packages/core/src/cli/telemetry.ts @@ -198,7 +198,7 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry { const publicKey = resolvePublicKey(); const disabled = telemetryDisabled() || !publicKey; const endpoint = resolveEndpoint(); - const installId = disabled ? "" : resolveInstallId(); + let installId: string | undefined = disabled ? "" : undefined; const runId = crypto.randomUUID(); const inFlight = new Set>(); @@ -211,18 +211,18 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry { ci: process.env.CI === "true", interactive: options.interactive, runId, - installId, }; function track(event: string, properties?: Record): void { if (disabled) return; + installId ??= resolveInstallId(); const body = JSON.stringify({ publicKey, event, anonymousId: installId, sessionId: runId, timestamp: new Date().toISOString(), - properties: { ...base, ...properties }, + properties: { ...base, installId, ...properties }, }); const promise = fetch(endpoint, { method: "POST", diff --git a/packages/skills/src/telemetry.spec.ts b/packages/skills/src/telemetry.spec.ts index d8740e0674..19f2365762 100644 --- a/packages/skills/src/telemetry.spec.ts +++ b/packages/skills/src/telemetry.spec.ts @@ -59,6 +59,26 @@ describe("createCliTelemetry", () => { expect(fetchMock).not.toHaveBeenCalled(); }); + it("does not create an installation id until an event is tracked", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "an-telemetry-")); + process.env.HOME = home; + process.env.USERPROFILE = home; + process.env.NODE_ENV = "production"; + process.env.AGENT_NATIVE_ANALYTICS_PUBLIC_KEY = "anpk_unit_test_key"; + + createCliTelemetry({ + cli: "skills-installer", + cliVersion: "9.9.9", + command: "add", + interactive: false, + }); + + expect( + fs.existsSync(path.join(home, ".agent-native", "installation-id")), + ).toBe(false); + fs.rmSync(home, { recursive: true, force: true }); + }); + it("falls back to the embedded public key when no env override is set", async () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "an-telemetry-")); process.env.HOME = home; diff --git a/packages/skills/src/telemetry.ts b/packages/skills/src/telemetry.ts index f38eedf74d..bc407f4242 100644 --- a/packages/skills/src/telemetry.ts +++ b/packages/skills/src/telemetry.ts @@ -198,7 +198,7 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry { const publicKey = resolvePublicKey(); const disabled = telemetryDisabled() || !publicKey; const endpoint = resolveEndpoint(); - const installId = disabled ? "" : resolveInstallId(); + let installId: string | undefined = disabled ? "" : undefined; const runId = crypto.randomUUID(); const inFlight = new Set>(); @@ -211,18 +211,18 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry { ci: process.env.CI === "true", interactive: options.interactive, runId, - installId, }; function track(event: string, properties?: Record): void { if (disabled) return; + installId ??= resolveInstallId(); const body = JSON.stringify({ publicKey, event, anonymousId: installId, sessionId: runId, timestamp: new Date().toISOString(), - properties: { ...base, ...properties }, + properties: { ...base, installId, ...properties }, }); const promise = fetch(endpoint, { method: "POST", From e95ad0796bf5d55b2acc72558de2c0daf1a95b8d Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:41:14 -0400 Subject: [PATCH 10/11] fix: preflight rewind before cli telemetry --- packages/core/src/cli/index.ts | 3 ++- .../core/src/cli/telemetry-routing.spec.ts | 18 ++++++++++++++++++ packages/core/src/cli/telemetry-routing.ts | 11 +++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/cli/telemetry-routing.spec.ts create mode 100644 packages/core/src/cli/telemetry-routing.ts diff --git a/packages/core/src/cli/index.ts b/packages/core/src/cli/index.ts index b35d629fbb..c4ff45c6bd 100644 --- a/packages/core/src/cli/index.ts +++ b/packages/core/src/cli/index.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from "url"; import * as Sentry from "@sentry/node"; import { resolveDeployPostBuildInvocation } from "./deploy-build.js"; +import { shouldTrackCliRun } from "./telemetry-routing.js"; import { createCliTelemetry } from "./telemetry.js"; // Resolve version once at module scope — used by both --version and --help @@ -604,7 +605,7 @@ function runBuildStep( }); } -trackCli("cli.run"); +if (shouldTrackCliRun(command, args)) trackCli("cli.run"); switch (command) { case "dev": { diff --git a/packages/core/src/cli/telemetry-routing.spec.ts b/packages/core/src/cli/telemetry-routing.spec.ts new file mode 100644 index 0000000000..461ae1e9a3 --- /dev/null +++ b/packages/core/src/cli/telemetry-routing.spec.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; + +import { shouldTrackCliRun } from "./telemetry-routing.js"; + +describe("shouldTrackCliRun", () => { + it.each([ + ["skills", ["add", "rewind", "--yes"]], + ["skills", ["add", "--skill", "rewind", "--yes"]], + ["skills", ["add", "--skill=rewind", "--yes"]], + ])("defers telemetry for explicit Rewind installs", (command, args) => { + expect(shouldTrackCliRun(command, args)).toBe(false); + }); + + it("tracks other CLI invocations immediately", () => { + expect(shouldTrackCliRun("skills", ["add", "visual-plan"])).toBe(true); + expect(shouldTrackCliRun("create", ["my-app"])).toBe(true); + }); +}); diff --git a/packages/core/src/cli/telemetry-routing.ts b/packages/core/src/cli/telemetry-routing.ts new file mode 100644 index 0000000000..e4ced0b322 --- /dev/null +++ b/packages/core/src/cli/telemetry-routing.ts @@ -0,0 +1,11 @@ +export function shouldTrackCliRun(command: string | undefined, args: string[]) { + if (command !== "skills" || args[0] !== "add") return true; + + const explicitlyTargetsRewind = args.some( + (arg, index) => + arg === "rewind" || + arg === "--skill=rewind" || + (arg === "--skill" && args[index + 1] === "rewind"), + ); + return !explicitlyTargetsRewind; +} From e961c31c5af1ed11811bd7ba72ca014a3728cfff Mon Sep 17 00:00:00 2001 From: Alice Alexandra Moore <86723305+3mdistal@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:50:14 -0400 Subject: [PATCH 11/11] fix: roll back partial rewind setup --- packages/core/src/cli/skills.spec.ts | 75 +++++++++++ packages/core/src/cli/skills.ts | 179 ++++++++++++++++++++++----- 2 files changed, 226 insertions(+), 28 deletions(-) diff --git a/packages/core/src/cli/skills.spec.ts b/packages/core/src/cli/skills.spec.ts index 07639f82dc..625bbd0b40 100644 --- a/packages/core/src/cli/skills.spec.ts +++ b/packages/core/src/cli/skills.spec.ts @@ -434,6 +434,81 @@ describe("agent-native skills", () => { } }); + it.each(["codex", "claude-code"] as const)( + "restores existing %s instructions and config when MCP setup fails", + async (client) => { + const root = tmpDir(); + const home = path.join(root, "home"); + const codexHome = path.join(root, "codex-home"); + const store = path.join(root, "screen-memory"); + const skillDir = + client === "codex" + ? path.join(codexHome, "skills", "rewind") + : path.join(home, ".claude", "skills", "rewind"); + const configPath = + client === "codex" + ? path.join(codexHome, "config.toml") + : path.join(home, ".claude.json"); + fs.mkdirSync(skillDir, { recursive: true }); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.mkdirSync(store, { recursive: true }); + fs.writeFileSync(path.join(skillDir, "SKILL.md"), "original skill\n"); + fs.writeFileSync(configPath, "original config\n"); + const previousHome = process.env.HOME; + const previousCodexHome = process.env.CODEX_HOME; + const previousStore = process.env.CLIPS_SCREEN_MEMORY_DIR; + process.env.HOME = home; + process.env.CODEX_HOME = codexHome; + process.env.CLIPS_SCREEN_MEMORY_DIR = store; + const telemetry = { + track: vi.fn(), + captureException: vi.fn(), + flush: vi.fn(async () => undefined), + }; + + try { + await expect( + addAgentNativeSkill( + parseSkillsArgs([ + "add", + "rewind", + "--client", + client, + "--scope", + "user", + "--yes", + ]), + { + baseDir: root, + telemetry, + installScreenMemory: () => { + fs.writeFileSync(configPath, "partial config\n"); + throw new Error("simulated MCP write failure"); + }, + }, + ), + ).rejects.toThrow("simulated MCP write failure"); + + expect(fs.readFileSync(configPath, "utf-8")).toBe("original config\n"); + expect(fs.readFileSync(path.join(skillDir, "SKILL.md"), "utf-8")).toBe( + "original skill\n", + ); + expect(telemetry.track).not.toHaveBeenCalledWith( + "skills_cli install completed", + expect.anything(), + ); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + if (previousCodexHome === undefined) delete process.env.CODEX_HOME; + else process.env.CODEX_HOME = previousCodexHome; + if (previousStore === undefined) + delete process.env.CLIPS_SCREEN_MEMORY_DIR; + else process.env.CLIPS_SCREEN_MEMORY_DIR = previousStore; + } + }, + ); + it("dry-runs Rewind setup without requiring an active local store", async () => { const result = await addAgentNativeSkill( parseSkillsArgs([ diff --git a/packages/core/src/cli/skills.ts b/packages/core/src/cli/skills.ts index 9fa0a67947..d4c2aec83e 100644 --- a/packages/core/src/cli/skills.ts +++ b/packages/core/src/cli/skills.ts @@ -32,7 +32,7 @@ import { CONTEXT_XRAY_SKILL_MD, installLocalContextXray, } from "./context-xray-local.js"; -import { CLIENTS, type ClientId } from "./mcp-config-writers.js"; +import { CLIENTS, configPathFor, type ClientId } from "./mcp-config-writers.js"; import { installScreenMemoryForClient, resolveScreenMemoryStoreDir, @@ -800,6 +800,7 @@ export interface RunSkillsOptions { * browser/device OAuth round-trip. */ runConnect?: (args: string[]) => Promise; + installScreenMemory?: typeof installScreenMemoryForClient; /** * Best-effort install-funnel telemetry. Created once per `runSkills` run and * threaded through resolution/install/connect so each `track` is fire-and- @@ -1405,7 +1406,7 @@ $ARGUMENTS * there is no need to shell out to the separate @agent-native/skills installer * (which would have to be published to npm first). Returns the written folders. */ -function installBuiltInInstructions(input: { +type BuiltInInstructionInstallInput = { appSkillId: BuiltInAppSkillId; onlySkillNames?: string[]; skillsAgents: string[]; @@ -1414,7 +1415,11 @@ function installBuiltInInstructions(input: { dryRun?: boolean; planMode?: PlanInstallMode; mcpUrl?: string; -}): string[] { +}; + +function builtInInstructionPaths( + input: BuiltInInstructionInstallInput, +): string[] { const bundles = Object.values( skillFilesForBuiltIn(input.appSkillId, { planMode: input.planMode, @@ -1434,15 +1439,10 @@ function installBuiltInInstructions(input: { ); for (const bundle of bundles) { const dir = path.join(root, bundle.skillName); - if (!input.dryRun) writeSkillFolder(dir, bundle); written.push(dir); const command = slashCommandForBuiltInSkill(bundle.skillName); if (command) { const commandPath = path.join(commandsRoot, `${bundle.skillName}.md`); - if (!input.dryRun) { - fs.mkdirSync(path.dirname(commandPath), { recursive: true }); - fs.writeFileSync(commandPath, command, "utf-8"); - } written.push(commandPath); } } @@ -1450,6 +1450,87 @@ function installBuiltInInstructions(input: { return written; } +function installBuiltInInstructions( + input: BuiltInInstructionInstallInput, +): string[] { + const bundles = Object.values( + skillFilesForBuiltIn(input.appSkillId, { + planMode: input.planMode, + mcpUrl: input.mcpUrl, + }), + ).filter( + (bundle) => + !input.onlySkillNames || input.onlySkillNames.includes(bundle.skillName), + ); + const written = builtInInstructionPaths(input); + if (input.dryRun) return written; + + for (const agent of input.skillsAgents) { + const root = builtInSkillsRootForAgent(agent, input.scope, input.baseDir); + const commandsRoot = builtInCommandsRootForAgent( + agent, + input.scope, + input.baseDir, + ); + for (const bundle of bundles) { + writeSkillFolder(path.join(root, bundle.skillName), bundle); + const command = slashCommandForBuiltInSkill(bundle.skillName); + if (command) { + const commandPath = path.join(commandsRoot, `${bundle.skillName}.md`); + fs.mkdirSync(path.dirname(commandPath), { recursive: true }); + fs.writeFileSync(commandPath, command, "utf-8"); + } + } + } + return written; +} + +interface InstallPathSnapshot { + target: string; + backup: string; + existed: boolean; +} + +function snapshotInstallPaths( + targets: string[], + backupRoot: string, +): InstallPathSnapshot[] { + return [...new Set(targets)].map((target, index) => { + const backup = path.join(backupRoot, `snapshot-${index}`); + const existed = fs.existsSync(target); + if (existed) fs.cpSync(target, backup, { recursive: true }); + return { target, backup, existed }; + }); +} + +function removeEmptyParents(start: string, boundary: string): void { + let current = path.resolve(start); + const stop = path.resolve(boundary); + while (current !== stop && current.startsWith(`${stop}${path.sep}`)) { + if (!fs.existsSync(current) || fs.readdirSync(current).length > 0) return; + fs.rmdirSync(current); + current = path.dirname(current); + } +} + +function restoreInstallPaths( + snapshots: InstallPathSnapshot[], + boundary: string, +): void { + for (const snapshot of snapshots.toReversed()) { + fs.rmSync(snapshot.target, { recursive: true, force: true }); + if (snapshot.existed) { + fs.mkdirSync(path.dirname(snapshot.target), { recursive: true }); + fs.cpSync(snapshot.backup, snapshot.target, { recursive: true }); + } + } + for (const snapshot of snapshots) { + if (!snapshot.existed) { + removeEmptyParents(path.dirname(snapshot.target), boundary); + } + } +} + function listSkillFolderFiles(dir: string): Record { const out: Record = {}; const walk = (current: string, prefix = "") => { @@ -3573,6 +3654,36 @@ export async function addAgentNativeSkill( let connectCommand: string | undefined; let registeredMcpClients: ClientId[] = shouldRegisterMcp ? mcpClients : []; let localManifestPath: string | undefined; + const builtInInstructionInput: BuiltInInstructionInstallInput | undefined = + knownTarget + ? { + appSkillId: knownTarget, + onlySkillNames, + skillsAgents, + scope: parsed.scope as "project" | "user", + baseDir, + dryRun: parsed.dryRun, + planMode, + mcpUrl: installTarget.loaded.manifest.hosted.mcpUrl, + } + : undefined; + const rewindSnapshots = installsScreenMemoryMcp + ? snapshotInstallPaths( + [ + ...(parsed.instructions && builtInInstructionInput + ? builtInInstructionPaths(builtInInstructionInput) + : []), + ...(shouldRegisterMcp + ? mcpClients.map((client) => + configPathFor(client, baseDir, parsed.scope), + ) + : []), + ], + tmpRoot, + ) + : undefined; + const rollbackBoundary = + parsed.scope === "user" ? os.homedir() : path.resolve(baseDir); try { if (parsed.instructions) { @@ -3582,21 +3693,14 @@ export async function addAgentNativeSkill( "Skill instructions use shared .agents for Codex, Pi, Cursor, OpenCode, Copilot, and similar agents, or Claude Code's native files. Use an MCP-capable client or omit --instructions-only.", ); } - } else if (knownTarget) { + } else if (knownTarget && builtInInstructionInput) { // Built-in skills ship their instructions inside this package, so copy // the skill folders straight into each client's skills directory. This // avoids shelling out to the separate @agent-native/skills installer // (which would need to be published to npm to run via npx). - instructionsWritten = installBuiltInInstructions({ - appSkillId: knownTarget, - onlySkillNames, - skillsAgents, - scope: parsed.scope as "project" | "user", - baseDir, - dryRun: parsed.dryRun, - planMode, - mcpUrl: installTarget.loaded.manifest.hosted.mcpUrl, - }); + instructionsWritten = installBuiltInInstructions( + builtInInstructionInput, + ); instructionSource = instructionsWritten[0]; commands.push(...instructionsWritten.map((dir) => `write ${dir}`)); } else { @@ -3640,21 +3744,22 @@ export async function addAgentNativeSkill( commands.push(`write ${localManifestPath}`); } - // Skill instructions are now on disk (built-in folders copied or external - // pack materialized) — record the install before MCP registration/connect. - options.telemetry?.track("skills_cli install completed", { - skills: installTarget.skillNames.join(","), - clients: clients.join(","), - scope: parsed.scope, - dryRun: Boolean(parsed.dryRun), - }); + // Rewind reports completion only after both local writes succeed. + if (!installsScreenMemoryMcp) { + options.telemetry?.track("skills_cli install completed", { + skills: installTarget.skillNames.join(","), + clients: clients.join(","), + scope: parsed.scope, + dryRun: Boolean(parsed.dryRun), + }); + } if (shouldRegisterMcp && installsScreenMemoryMcp) { registeredMcpClients = mcpClients.map((client) => { commands.push( `npx @agent-native/core@latest mcp install-screen-memory --client ${client} --scope ${parsed.scope}`, ); - installScreenMemoryForClient( + (options.installScreenMemory ?? installScreenMemoryForClient)( client, screenMemoryDir!, baseDir, @@ -3665,6 +3770,12 @@ export async function addAgentNativeSkill( options.telemetry?.track("skills_cli mcp registered", { skills: installTarget.skillNames.join(","), }); + options.telemetry?.track("skills_cli install completed", { + skills: installTarget.skillNames.join(","), + clients: clients.join(","), + scope: parsed.scope, + dryRun: false, + }); } else if (shouldRegisterMcp) { commands.push( `npx @agent-native/core@latest app-skill ensure --manifest ${installTarget.loaded.file} --client ${parsed.client} --scope ${parsed.scope} --yes`, @@ -3792,6 +3903,18 @@ export async function addAgentNativeSkill( githubActionExisted, githubActionSuggestedCommand, }; + } catch (error) { + if (rewindSnapshots) { + try { + restoreInstallPaths(rewindSnapshots, rollbackBoundary); + } catch (rollbackError) { + throw new AggregateError( + [error, rollbackError], + "Rewind setup failed and its partial installation could not be fully rolled back.", + ); + } + } + throw error; } finally { fs.rmSync(tmpRoot, { recursive: true, force: true }); installTarget.cleanup?.();