@agent-native/core 0.122.3 → 0.122.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/index.ts +2 -1
  5. package/corpus/core/src/cli/mcp.ts +13 -3
  6. package/corpus/core/src/cli/skills-content/rewind-skill.ts +36 -4
  7. package/corpus/core/src/cli/skills.ts +250 -37
  8. package/corpus/core/src/cli/telemetry-routing.ts +57 -0
  9. package/corpus/core/src/cli/telemetry.ts +3 -3
  10. package/corpus/core/src/mcp/screen-memory-stdio.ts +21 -2
  11. package/dist/cli/index.js +3 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/mcp.d.ts.map +1 -1
  14. package/dist/cli/mcp.js +12 -3
  15. package/dist/cli/mcp.js.map +1 -1
  16. package/dist/cli/skills-content/rewind-skill.d.ts +1 -1
  17. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -1
  18. package/dist/cli/skills-content/rewind-skill.js +36 -4
  19. package/dist/cli/skills-content/rewind-skill.js.map +1 -1
  20. package/dist/cli/skills.d.ts +2 -0
  21. package/dist/cli/skills.d.ts.map +1 -1
  22. package/dist/cli/skills.js +191 -40
  23. package/dist/cli/skills.js.map +1 -1
  24. package/dist/cli/telemetry-routing.d.ts +2 -0
  25. package/dist/cli/telemetry-routing.d.ts.map +1 -0
  26. package/dist/cli/telemetry-routing.js +54 -0
  27. package/dist/cli/telemetry-routing.js.map +1 -0
  28. package/dist/cli/telemetry.js +3 -3
  29. package/dist/cli/telemetry.js.map +1 -1
  30. package/dist/collab/routes.d.ts +1 -1
  31. package/dist/collab/struct-routes.d.ts +1 -1
  32. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  33. package/dist/mcp/screen-memory-stdio.d.ts +2 -0
  34. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  35. package/dist/mcp/screen-memory-stdio.js +12 -2
  36. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  37. package/dist/observability/routes.d.ts +1 -1
  38. package/dist/progress/routes.d.ts +1 -1
  39. package/package.json +1 -1
  40. package/src/cli/index.ts +2 -1
  41. package/src/cli/mcp.ts +13 -3
  42. package/src/cli/skills-content/rewind-skill.ts +36 -4
  43. package/src/cli/skills.ts +250 -37
  44. package/src/cli/telemetry-routing.ts +57 -0
  45. package/src/cli/telemetry.ts +3 -3
  46. package/src/mcp/screen-memory-stdio.ts +21 -2
package/corpus/README.md CHANGED
@@ -28,6 +28,6 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
 
29
29
  ## Generated Counts
30
30
 
31
- - core files: 1673
31
+ - core files: 1674
32
32
  - toolkit files: 160
33
33
  - template files: 6472
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.122.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 648c2d7: Install Rewind through Agent Native's complete local Screen Memory setup instead of treating it as a plain public skill, route every supported Rewind name through the same fail-before-writes checks, reject missing or disconnected local stores, keep explicit-store retrieval bound to Clips' feature configuration, and guide first-time users through consent-gated Clips Desktop setup.
8
+
3
9
  ## 0.122.3
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.122.3",
3
+ "version": "0.122.4",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -9,6 +9,7 @@ import { fileURLToPath } from "url";
9
9
  import * as Sentry from "@sentry/node";
10
10
 
11
11
  import { resolveDeployPostBuildInvocation } from "./deploy-build.js";
12
+ import { shouldTrackCliRun } from "./telemetry-routing.js";
12
13
  import { createCliTelemetry } from "./telemetry.js";
13
14
 
14
15
  // Resolve version once at module scope — used by both --version and --help
@@ -604,7 +605,7 @@ function runBuildStep(
604
605
  });
605
606
  }
606
607
 
607
- trackCli("cli.run");
608
+ if (shouldTrackCliRun(command, args)) trackCli("cli.run");
608
609
 
609
610
  switch (command) {
610
611
  case "dev": {
@@ -92,6 +92,15 @@ export interface ScreenMemoryStoreResolutionOptions {
92
92
  homeDir?: string;
93
93
  }
94
94
 
95
+ function existingScreenMemoryStore(candidate: string): string | undefined {
96
+ const resolved = path.resolve(candidate);
97
+ try {
98
+ return fs.statSync(resolved).isDirectory() ? resolved : undefined;
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ }
103
+
95
104
  /**
96
105
  * Resolve the active Clips store without asking people to find an app-data
97
106
  * path. Environment overrides remain the unambiguous escape hatch; otherwise
@@ -100,11 +109,12 @@ export interface ScreenMemoryStoreResolutionOptions {
100
109
  export function resolveScreenMemoryStoreDir(
101
110
  options: ScreenMemoryStoreResolutionOptions = {},
102
111
  ): string | undefined {
103
- if (options.explicitDir) return path.resolve(options.explicitDir);
112
+ if (options.explicitDir)
113
+ return existingScreenMemoryStore(options.explicitDir);
104
114
  const env = options.env ?? process.env;
105
115
  const override =
106
116
  env.CLIPS_SCREEN_MEMORY_DIR || env.AGENT_NATIVE_SCREEN_MEMORY_DIR;
107
- if (override) return path.resolve(override);
117
+ if (override) return existingScreenMemoryStore(override);
108
118
 
109
119
  const platform = options.platform ?? process.platform;
110
120
  const home = options.homeDir ?? os.homedir();
@@ -116,7 +126,7 @@ export function resolveScreenMemoryStoreDir(
116
126
  : env.XDG_DATA_HOME || path.join(home, ".local", "share");
117
127
  return ["com.clips.tray", "com.clips.tray.alpha"]
118
128
  .map((bundleId) => path.join(appDataRoot, bundleId, "screen-memory"))
119
- .filter((candidate) => fs.existsSync(candidate))
129
+ .filter((candidate) => existingScreenMemoryStore(candidate))
120
130
  .map((candidate) => ({
121
131
  candidate,
122
132
  modifiedAt: Math.max(
@@ -13,6 +13,38 @@ metadata:
13
13
  Use Clips Rewind as local screen memory. Start broad enough to find the right
14
14
  moment, then read only the smallest relevant range.
15
15
 
16
+ ## First-Run Setup
17
+
18
+ Call \`screen_memory_status\` before assuming Rewind is ready. If the tool is
19
+ unavailable, do not stop at a missing-tool error and do not silently install
20
+ anything:
21
+
22
+ 1. Explain that Rewind requires the signed Clips Desktop app on macOS plus a
23
+ local agent connection.
24
+ 2. Ask whether Clips is installed, or use a non-mutating native application
25
+ lookup when the host supports one. Do not inspect Clips' app-data folders.
26
+ If Clips is absent or its presence cannot be confirmed, ask permission
27
+ before opening or downloading anything: “Clips Desktop is required for
28
+ Rewind. Would you like me to open the official installer and guide you
29
+ through setup?”
30
+ 3. Only after affirmative permission, open
31
+ \`https://clips.agent-native.com/download\` in the user's browser. Complete
32
+ native installation steps only when the current host can safely do so and
33
+ the user explicitly permitted them. Never bypass Gatekeeper, accept macOS
34
+ permission prompts, or enable screen/audio capture on the user's behalf.
35
+ 4. If native UI control is unavailable, return the download link and wait for
36
+ the user to confirm that Clips is installed and running.
37
+ 5. Ask the user to enable Rewind in the Clips tray and choose the intended
38
+ capture mode. Then configure the local connection with:
39
+
40
+ \`npx -y @agent-native/core@latest skills add rewind --client <client> --scope user --yes\`
41
+
42
+ Replace \`<client>\` with the current compatible host: \`codex\`,
43
+ \`claude-code\`, \`cursor\`, \`opencode\`, \`github-copilot\`, or \`cowork\`.
44
+ 6. Restart the host only if it cannot reload MCP servers in place, then retry
45
+ \`screen_memory_status\`. Do not claim setup succeeded until it reports
46
+ Rewind enabled and unpaused.
47
+
16
48
  ## Retrieval Flow
17
49
 
18
50
  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.
41
73
  bypass the Screen Memory MCP broker.
42
74
  - Do not upload frames returned by local Screen Memory tools.
43
75
  - Treat foreground apps and chapter labels as evidence, not proof of intent.
44
- - If the Screen Memory MCP is missing, explain that the one-time setup needs to
45
- be repaired with:
76
+ - If Clips is installed and Rewind is enabled but the Screen Memory MCP is
77
+ missing, explain that only the agent connection needs repair with:
46
78
 
47
79
  \`npx -y @agent-native/core@latest skills add rewind --client <client> --scope user --yes\`
48
80
 
49
- Replace \`<client>\` with the current compatible host: \`codex\`,
50
- \`claude-code\`, \`cursor\`, \`opencode\`, \`github-copilot\`, or \`cowork\`.
51
81
  Ask the user to restart the host if it cannot reload MCP servers in place.
82
+ - Never conflate installing Clips Desktop with installing the skill/MCP
83
+ connection, and never claim either succeeded without direct evidence.
52
84
  `;
@@ -32,7 +32,7 @@ import {
32
32
  CONTEXT_XRAY_SKILL_MD,
33
33
  installLocalContextXray,
34
34
  } from "./context-xray-local.js";
35
- import { CLIENTS, type ClientId } from "./mcp-config-writers.js";
35
+ import { CLIENTS, configPathFor, type ClientId } from "./mcp-config-writers.js";
36
36
  import {
37
37
  installScreenMemoryForClient,
38
38
  resolveScreenMemoryStoreDir,
@@ -800,6 +800,7 @@ export interface RunSkillsOptions {
800
800
  * browser/device OAuth round-trip.
801
801
  */
802
802
  runConnect?: (args: string[]) => Promise<void>;
803
+ installScreenMemory?: typeof installScreenMemoryForClient;
803
804
  /**
804
805
  * Best-effort install-funnel telemetry. Created once per `runSkills` run and
805
806
  * threaded through resolution/install/connect so each `track` is fire-and-
@@ -850,6 +851,50 @@ function isKnownSkill(value: string | undefined): boolean {
850
851
  return Boolean(normalizeKnownSkillTarget(value));
851
852
  }
852
853
 
854
+ function explicitlyTargetsRewind(parsed: ParsedSkillsArgs): boolean {
855
+ return (
856
+ normalizeKnownSkillTarget(parsed.target ?? "assets") === "rewind" ||
857
+ Boolean(
858
+ parsed.plainSkillNames?.some(
859
+ (skillName) => normalizeKnownSkillTarget(skillName) === "rewind",
860
+ ),
861
+ )
862
+ );
863
+ }
864
+
865
+ const REWIND_MISSING_STORE_ERROR =
866
+ "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.";
867
+
868
+ function preflightRewindStore(parsed: ParsedSkillsArgs): string | undefined {
869
+ if (parsed.command !== "add" || !explicitlyTargetsRewind(parsed))
870
+ return undefined;
871
+ if (!parsed.mcp) {
872
+ throw new Error(
873
+ "Rewind requires the local Clips Screen Memory MCP and cannot be installed with --no-mcp.",
874
+ );
875
+ }
876
+ if (parsed.mcpUrl) {
877
+ throw new Error(
878
+ "Rewind uses the local Clips Screen Memory MCP and does not accept --mcp-url.",
879
+ );
880
+ }
881
+ if (parsed.dryRun) return undefined;
882
+ const screenMemoryDir = resolveScreenMemoryStoreDir();
883
+ if (!screenMemoryDir) throw new Error(REWIND_MISSING_STORE_ERROR);
884
+ return screenMemoryDir;
885
+ }
886
+
887
+ function preflightResolvedRewindTargets(
888
+ parsed: ParsedSkillsArgs,
889
+ targets: string[],
890
+ ): void {
891
+ preflightRewindStore({
892
+ ...parsed,
893
+ target: undefined,
894
+ plainSkillNames: [...(parsed.plainSkillNames ?? []), ...targets],
895
+ });
896
+ }
897
+
853
898
  function isLocalOnlyBuiltInSkill(
854
899
  entry: (typeof BUILT_IN_APP_SKILLS)[BuiltInAppSkillId] | null | undefined,
855
900
  ): boolean {
@@ -1382,7 +1427,7 @@ $ARGUMENTS
1382
1427
  * there is no need to shell out to the separate @agent-native/skills installer
1383
1428
  * (which would have to be published to npm first). Returns the written folders.
1384
1429
  */
1385
- function installBuiltInInstructions(input: {
1430
+ type BuiltInInstructionInstallInput = {
1386
1431
  appSkillId: BuiltInAppSkillId;
1387
1432
  onlySkillNames?: string[];
1388
1433
  skillsAgents: string[];
@@ -1391,7 +1436,11 @@ function installBuiltInInstructions(input: {
1391
1436
  dryRun?: boolean;
1392
1437
  planMode?: PlanInstallMode;
1393
1438
  mcpUrl?: string;
1394
- }): string[] {
1439
+ };
1440
+
1441
+ function builtInInstructionPaths(
1442
+ input: BuiltInInstructionInstallInput,
1443
+ ): string[] {
1395
1444
  const bundles = Object.values(
1396
1445
  skillFilesForBuiltIn(input.appSkillId, {
1397
1446
  planMode: input.planMode,
@@ -1411,15 +1460,10 @@ function installBuiltInInstructions(input: {
1411
1460
  );
1412
1461
  for (const bundle of bundles) {
1413
1462
  const dir = path.join(root, bundle.skillName);
1414
- if (!input.dryRun) writeSkillFolder(dir, bundle);
1415
1463
  written.push(dir);
1416
1464
  const command = slashCommandForBuiltInSkill(bundle.skillName);
1417
1465
  if (command) {
1418
1466
  const commandPath = path.join(commandsRoot, `${bundle.skillName}.md`);
1419
- if (!input.dryRun) {
1420
- fs.mkdirSync(path.dirname(commandPath), { recursive: true });
1421
- fs.writeFileSync(commandPath, command, "utf-8");
1422
- }
1423
1467
  written.push(commandPath);
1424
1468
  }
1425
1469
  }
@@ -1427,6 +1471,87 @@ function installBuiltInInstructions(input: {
1427
1471
  return written;
1428
1472
  }
1429
1473
 
1474
+ function installBuiltInInstructions(
1475
+ input: BuiltInInstructionInstallInput,
1476
+ ): string[] {
1477
+ const bundles = Object.values(
1478
+ skillFilesForBuiltIn(input.appSkillId, {
1479
+ planMode: input.planMode,
1480
+ mcpUrl: input.mcpUrl,
1481
+ }),
1482
+ ).filter(
1483
+ (bundle) =>
1484
+ !input.onlySkillNames || input.onlySkillNames.includes(bundle.skillName),
1485
+ );
1486
+ const written = builtInInstructionPaths(input);
1487
+ if (input.dryRun) return written;
1488
+
1489
+ for (const agent of input.skillsAgents) {
1490
+ const root = builtInSkillsRootForAgent(agent, input.scope, input.baseDir);
1491
+ const commandsRoot = builtInCommandsRootForAgent(
1492
+ agent,
1493
+ input.scope,
1494
+ input.baseDir,
1495
+ );
1496
+ for (const bundle of bundles) {
1497
+ writeSkillFolder(path.join(root, bundle.skillName), bundle);
1498
+ const command = slashCommandForBuiltInSkill(bundle.skillName);
1499
+ if (command) {
1500
+ const commandPath = path.join(commandsRoot, `${bundle.skillName}.md`);
1501
+ fs.mkdirSync(path.dirname(commandPath), { recursive: true });
1502
+ fs.writeFileSync(commandPath, command, "utf-8");
1503
+ }
1504
+ }
1505
+ }
1506
+ return written;
1507
+ }
1508
+
1509
+ interface InstallPathSnapshot {
1510
+ target: string;
1511
+ backup: string;
1512
+ existed: boolean;
1513
+ }
1514
+
1515
+ function snapshotInstallPaths(
1516
+ targets: string[],
1517
+ backupRoot: string,
1518
+ ): InstallPathSnapshot[] {
1519
+ return [...new Set(targets)].map((target, index) => {
1520
+ const backup = path.join(backupRoot, `snapshot-${index}`);
1521
+ const existed = fs.existsSync(target);
1522
+ if (existed) fs.cpSync(target, backup, { recursive: true });
1523
+ return { target, backup, existed };
1524
+ });
1525
+ }
1526
+
1527
+ function removeEmptyParents(start: string, boundary: string): void {
1528
+ let current = path.resolve(start);
1529
+ const stop = path.resolve(boundary);
1530
+ while (current !== stop && current.startsWith(`${stop}${path.sep}`)) {
1531
+ if (!fs.existsSync(current) || fs.readdirSync(current).length > 0) return;
1532
+ fs.rmdirSync(current);
1533
+ current = path.dirname(current);
1534
+ }
1535
+ }
1536
+
1537
+ function restoreInstallPaths(
1538
+ snapshots: InstallPathSnapshot[],
1539
+ boundary: string,
1540
+ ): void {
1541
+ for (const snapshot of snapshots.toReversed()) {
1542
+ fs.rmSync(snapshot.target, { recursive: true, force: true });
1543
+ if (snapshot.existed) {
1544
+ fs.mkdirSync(path.dirname(snapshot.target), { recursive: true });
1545
+ fs.cpSync(snapshot.backup, snapshot.target, { recursive: true });
1546
+ }
1547
+ }
1548
+ for (const snapshot of snapshots) {
1549
+ if (!snapshot.existed) {
1550
+ removeEmptyParents(path.dirname(snapshot.target), boundary);
1551
+ }
1552
+ }
1553
+ }
1554
+
1430
1555
  function listSkillFolderFiles(dir: string): Record<string, string> {
1431
1556
  const out: Record<string, string> = {};
1432
1557
  const walk = (current: string, prefix = "") => {
@@ -3376,6 +3501,11 @@ export async function addAgentNativeSkill(
3376
3501
  const knownBuiltIn = knownTarget ? BUILT_IN_APP_SKILLS[knownTarget] : null;
3377
3502
  const installsScreenMemoryMcp = isScreenMemoryMcpBuiltInSkill(knownBuiltIn);
3378
3503
  const baseDir = options.baseDir ?? process.cwd();
3504
+ if (installsScreenMemoryMcp && !parsed.mcp) {
3505
+ throw new Error(
3506
+ "Rewind requires the local Clips Screen Memory MCP and cannot be installed with --no-mcp.",
3507
+ );
3508
+ }
3379
3509
  if (installsScreenMemoryMcp && parsed.mcpUrl) {
3380
3510
  throw new Error(
3381
3511
  "Rewind uses the local Clips Screen Memory MCP and does not accept --mcp-url.",
@@ -3487,7 +3617,7 @@ export async function addAgentNativeSkill(
3487
3617
  }
3488
3618
  installTarget = preserveMcpUrlAppPathOverride(installTarget, parsed.mcpUrl);
3489
3619
  const skillsAgents = installsScreenMemoryMcp
3490
- ? clients
3620
+ ? clients.filter((client) => client !== "cowork")
3491
3621
  : skillsAgentsForClients(clients);
3492
3622
  if (parsed.dryRun) {
3493
3623
  try {
@@ -3537,6 +3667,7 @@ export async function addAgentNativeSkill(
3537
3667
  }
3538
3668
  }
3539
3669
  const commands: string[] = [];
3670
+ const screenMemoryDir = preflightRewindStore(parsed);
3540
3671
  const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "an-skills-add-"));
3541
3672
  let instructionSource: string | undefined;
3542
3673
  let instructionsWritten: string[] | undefined;
@@ -3544,6 +3675,36 @@ export async function addAgentNativeSkill(
3544
3675
  let connectCommand: string | undefined;
3545
3676
  let registeredMcpClients: ClientId[] = shouldRegisterMcp ? mcpClients : [];
3546
3677
  let localManifestPath: string | undefined;
3678
+ const builtInInstructionInput: BuiltInInstructionInstallInput | undefined =
3679
+ knownTarget
3680
+ ? {
3681
+ appSkillId: knownTarget,
3682
+ onlySkillNames,
3683
+ skillsAgents,
3684
+ scope: parsed.scope as "project" | "user",
3685
+ baseDir,
3686
+ dryRun: parsed.dryRun,
3687
+ planMode,
3688
+ mcpUrl: installTarget.loaded.manifest.hosted.mcpUrl,
3689
+ }
3690
+ : undefined;
3691
+ const rewindSnapshots = installsScreenMemoryMcp
3692
+ ? snapshotInstallPaths(
3693
+ [
3694
+ ...(parsed.instructions && builtInInstructionInput
3695
+ ? builtInInstructionPaths(builtInInstructionInput)
3696
+ : []),
3697
+ ...(shouldRegisterMcp
3698
+ ? mcpClients.map((client) =>
3699
+ configPathFor(client, baseDir, parsed.scope),
3700
+ )
3701
+ : []),
3702
+ ],
3703
+ tmpRoot,
3704
+ )
3705
+ : undefined;
3706
+ const rollbackBoundary =
3707
+ parsed.scope === "user" ? os.homedir() : path.resolve(baseDir);
3547
3708
 
3548
3709
  try {
3549
3710
  if (parsed.instructions) {
@@ -3553,21 +3714,14 @@ export async function addAgentNativeSkill(
3553
3714
  "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.",
3554
3715
  );
3555
3716
  }
3556
- } else if (knownTarget) {
3717
+ } else if (knownTarget && builtInInstructionInput) {
3557
3718
  // Built-in skills ship their instructions inside this package, so copy
3558
3719
  // the skill folders straight into each client's skills directory. This
3559
3720
  // avoids shelling out to the separate @agent-native/skills installer
3560
3721
  // (which would need to be published to npm to run via npx).
3561
- instructionsWritten = installBuiltInInstructions({
3562
- appSkillId: knownTarget,
3563
- onlySkillNames,
3564
- skillsAgents,
3565
- scope: parsed.scope as "project" | "user",
3566
- baseDir,
3567
- dryRun: parsed.dryRun,
3568
- planMode,
3569
- mcpUrl: installTarget.loaded.manifest.hosted.mcpUrl,
3570
- });
3722
+ instructionsWritten = installBuiltInInstructions(
3723
+ builtInInstructionInput,
3724
+ );
3571
3725
  instructionSource = instructionsWritten[0];
3572
3726
  commands.push(...instructionsWritten.map((dir) => `write ${dir}`));
3573
3727
  } else {
@@ -3611,29 +3765,24 @@ export async function addAgentNativeSkill(
3611
3765
  commands.push(`write ${localManifestPath}`);
3612
3766
  }
3613
3767
 
3614
- // Skill instructions are now on disk (built-in folders copied or external
3615
- // pack materialized) — record the install before MCP registration/connect.
3616
- options.telemetry?.track("skills_cli install completed", {
3617
- skills: installTarget.skillNames.join(","),
3618
- clients: clients.join(","),
3619
- scope: parsed.scope,
3620
- dryRun: Boolean(parsed.dryRun),
3621
- });
3768
+ // Rewind reports completion only after both local writes succeed.
3769
+ if (!installsScreenMemoryMcp) {
3770
+ options.telemetry?.track("skills_cli install completed", {
3771
+ skills: installTarget.skillNames.join(","),
3772
+ clients: clients.join(","),
3773
+ scope: parsed.scope,
3774
+ dryRun: Boolean(parsed.dryRun),
3775
+ });
3776
+ }
3622
3777
 
3623
3778
  if (shouldRegisterMcp && installsScreenMemoryMcp) {
3624
- const screenMemoryDir = resolveScreenMemoryStoreDir();
3625
- if (!screenMemoryDir) {
3626
- throw new Error(
3627
- "No local Clips Screen Memory store was found. Turn Rewind on in Clips, then run the setup again.",
3628
- );
3629
- }
3630
3779
  registeredMcpClients = mcpClients.map((client) => {
3631
3780
  commands.push(
3632
3781
  `npx @agent-native/core@latest mcp install-screen-memory --client ${client} --scope ${parsed.scope}`,
3633
3782
  );
3634
- installScreenMemoryForClient(
3783
+ (options.installScreenMemory ?? installScreenMemoryForClient)(
3635
3784
  client,
3636
- screenMemoryDir,
3785
+ screenMemoryDir!,
3637
3786
  baseDir,
3638
3787
  parsed.scope,
3639
3788
  );
@@ -3642,6 +3791,12 @@ export async function addAgentNativeSkill(
3642
3791
  options.telemetry?.track("skills_cli mcp registered", {
3643
3792
  skills: installTarget.skillNames.join(","),
3644
3793
  });
3794
+ options.telemetry?.track("skills_cli install completed", {
3795
+ skills: installTarget.skillNames.join(","),
3796
+ clients: clients.join(","),
3797
+ scope: parsed.scope,
3798
+ dryRun: false,
3799
+ });
3645
3800
  } else if (shouldRegisterMcp) {
3646
3801
  commands.push(
3647
3802
  `npx @agent-native/core@latest app-skill ensure --manifest ${installTarget.loaded.file} --client ${parsed.client} --scope ${parsed.scope} --yes`,
@@ -3769,6 +3924,18 @@ export async function addAgentNativeSkill(
3769
3924
  githubActionExisted,
3770
3925
  githubActionSuggestedCommand,
3771
3926
  };
3927
+ } catch (error) {
3928
+ if (rewindSnapshots) {
3929
+ try {
3930
+ restoreInstallPaths(rewindSnapshots, rollbackBoundary);
3931
+ } catch (rollbackError) {
3932
+ throw new AggregateError(
3933
+ [error, rollbackError],
3934
+ "Rewind setup failed and its partial installation could not be fully rolled back.",
3935
+ );
3936
+ }
3937
+ }
3938
+ throw error;
3772
3939
  } finally {
3773
3940
  fs.rmSync(tmpRoot, { recursive: true, force: true });
3774
3941
  installTarget.cleanup?.();
@@ -4046,6 +4213,41 @@ function readCliVersion(): string {
4046
4213
  }
4047
4214
  }
4048
4215
 
4216
+ function deferCliTelemetry(target: CliTelemetry): {
4217
+ telemetry: CliTelemetry;
4218
+ commit: () => void;
4219
+ } {
4220
+ type TrackCall = Parameters<CliTelemetry["track"]>;
4221
+ type ExceptionCall = Parameters<CliTelemetry["captureException"]>;
4222
+ const trackCalls: TrackCall[] = [];
4223
+ const exceptionCalls: ExceptionCall[] = [];
4224
+ let committed = false;
4225
+
4226
+ return {
4227
+ telemetry: {
4228
+ track(...args) {
4229
+ if (committed) target.track(...args);
4230
+ else trackCalls.push(args);
4231
+ },
4232
+ captureException(...args) {
4233
+ if (committed) target.captureException(...args);
4234
+ else exceptionCalls.push(args);
4235
+ },
4236
+ async flush() {
4237
+ if (committed) await target.flush();
4238
+ },
4239
+ },
4240
+ commit() {
4241
+ if (committed) return;
4242
+ committed = true;
4243
+ for (const args of trackCalls) target.track(...args);
4244
+ for (const args of exceptionCalls) target.captureException(...args);
4245
+ trackCalls.length = 0;
4246
+ exceptionCalls.length = 0;
4247
+ },
4248
+ };
4249
+ }
4250
+
4049
4251
  export async function runSkills(
4050
4252
  argv: string[],
4051
4253
  options: RunSkillsOptions = {},
@@ -4054,6 +4256,7 @@ export async function runSkills(
4054
4256
  if (parsed.baseDir) {
4055
4257
  options = { ...options, baseDir: path.resolve(parsed.baseDir) };
4056
4258
  }
4259
+ preflightRewindStore(parsed);
4057
4260
  const clackForLog = parsed.printJson
4058
4261
  ? undefined
4059
4262
  : await import("@clack/prompts");
@@ -4089,7 +4292,7 @@ export async function runSkills(
4089
4292
  // finally so events send on success, error, and cancellation — the CLI is
4090
4293
  // short-lived, so flushing before exit is essential or the events never send.
4091
4294
  const startedAt = Date.now();
4092
- const telemetry =
4295
+ const telemetryTarget =
4093
4296
  options.telemetry ??
4094
4297
  createCliTelemetry({
4095
4298
  cli: "core",
@@ -4097,6 +4300,13 @@ export async function runSkills(
4097
4300
  command: parsed.command,
4098
4301
  interactive: shouldPrompt(parsed, options),
4099
4302
  });
4303
+ const deferredTelemetry = deferCliTelemetry(telemetryTarget);
4304
+ const telemetry = deferredTelemetry.telemetry;
4305
+ const deferUntilSkillSelection =
4306
+ parsed.command === "add" &&
4307
+ !parsed.target &&
4308
+ !(parsed.plainSkillNames?.length ?? 0);
4309
+ if (!deferUntilSkillSelection) deferredTelemetry.commit();
4100
4310
  const optionsWithTelemetry: RunSkillsOptions = {
4101
4311
  ...options,
4102
4312
  telemetry,
@@ -4137,9 +4347,12 @@ export async function runSkills(
4137
4347
 
4138
4348
  const targets = await resolveSkillTargets(parsed, optionsWithTelemetry);
4139
4349
  if (!targets) {
4350
+ deferredTelemetry.commit();
4140
4351
  telemetry.track("skills_cli cancelled", { step: "skills" });
4141
4352
  return;
4142
4353
  }
4354
+ preflightResolvedRewindTargets(parsed, targets);
4355
+ deferredTelemetry.commit();
4143
4356
  const preselected = Boolean(parsed.target);
4144
4357
  telemetry.track("skills_cli skills selected", {
4145
4358
  selected: targets.join(","),
@@ -0,0 +1,57 @@
1
+ const REWIND_SKILL_TARGETS = new Set([
2
+ "rewind",
3
+ "screen-memory",
4
+ "clips-rewind",
5
+ "agent-native-rewind",
6
+ ]);
7
+
8
+ function isRewindSkillTarget(value: string | undefined): boolean {
9
+ return REWIND_SKILL_TARGETS.has(value?.trim().toLowerCase() ?? "");
10
+ }
11
+
12
+ const SKILLS_VALUE_FLAGS = new Set([
13
+ "--client",
14
+ "--agent",
15
+ "-a",
16
+ "--scope",
17
+ "--cwd",
18
+ "--mcp-url",
19
+ "--mode",
20
+ ]);
21
+
22
+ function explicitSkillTargets(args: string[]): string[] {
23
+ const targets: string[] = [];
24
+ const start = args[0] === "add" ? 1 : 0;
25
+ for (let index = start; index < args.length; index++) {
26
+ const arg = args[index];
27
+ if (arg === "--skill" || arg === "-s") {
28
+ if (args[index + 1]) targets.push(args[++index]);
29
+ continue;
30
+ }
31
+ if (arg.startsWith("--skill=")) {
32
+ targets.push(arg.slice("--skill=".length));
33
+ continue;
34
+ }
35
+ if (SKILLS_VALUE_FLAGS.has(arg)) {
36
+ index++;
37
+ continue;
38
+ }
39
+ if (arg.startsWith("-")) continue;
40
+ targets.push(arg);
41
+ }
42
+ return targets;
43
+ }
44
+
45
+ export function shouldTrackCliRun(command: string | undefined, args: string[]) {
46
+ if (command !== "skills") return true;
47
+ if (
48
+ ["list", "status", "update", "help", "--help", "-h"].includes(args[0] ?? "")
49
+ )
50
+ return true;
51
+
52
+ const targets = explicitSkillTargets(args);
53
+ if (targets.length === 0) return false;
54
+
55
+ const explicitlyTargetsRewind = targets.some(isRewindSkillTarget);
56
+ return !explicitlyTargetsRewind;
57
+ }
@@ -198,7 +198,7 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry {
198
198
  const publicKey = resolvePublicKey();
199
199
  const disabled = telemetryDisabled() || !publicKey;
200
200
  const endpoint = resolveEndpoint();
201
- const installId = disabled ? "" : resolveInstallId();
201
+ let installId: string | undefined = disabled ? "" : undefined;
202
202
  const runId = crypto.randomUUID();
203
203
  const inFlight = new Set<Promise<void>>();
204
204
 
@@ -211,18 +211,18 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry {
211
211
  ci: process.env.CI === "true",
212
212
  interactive: options.interactive,
213
213
  runId,
214
- installId,
215
214
  };
216
215
 
217
216
  function track(event: string, properties?: Record<string, unknown>): void {
218
217
  if (disabled) return;
218
+ installId ??= resolveInstallId();
219
219
  const body = JSON.stringify({
220
220
  publicKey,
221
221
  event,
222
222
  anonymousId: installId,
223
223
  sessionId: runId,
224
224
  timestamp: new Date().toISOString(),
225
- properties: { ...base, ...properties },
225
+ properties: { ...base, installId, ...properties },
226
226
  });
227
227
  const promise = fetch(endpoint, {
228
228
  method: "POST",