@notis_ai/cli 0.2.0-beta.178.1 → 0.2.0-beta.1792022492092.1

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 (124) hide show
  1. package/README.md +548 -98
  2. package/config/portal-route-roots.json +47 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +32901 -5934
  4. package/dist/base-skills/notis-apps/SKILL.md +76 -51
  5. package/dist/base-skills/notis-apps/references/architecture.md +116 -164
  6. package/dist/base-skills/notis-apps/references/design.md +26 -107
  7. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  8. package/dist/base-skills/notis-apps/references/reading.md +52 -74
  9. package/dist/base-skills/notis-apps/references/release.md +105 -96
  10. package/dist/base-skills/notis-apps/references/sdk.md +115 -60
  11. package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
  12. package/dist/base-skills/notis-apps/references/views.md +145 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +29 -12
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
  15. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  16. package/dist/base-skills/notis-cli/references/native-databases.md +6 -5
  17. package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
  18. package/dist/base-skills/notis-query/SKILL.md +34 -60
  19. package/dist/base-skills/notis-query/references/database-discovery.md +60 -59
  20. package/dist/base-skills/notis-query/references/documents.md +53 -39
  21. package/dist/base-skills/notis-query/references/query.md +57 -524
  22. package/dist/sdk/src/bundleModule.ts +79 -0
  23. package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
  24. package/dist/sdk/src/components/DocumentPage.tsx +144 -0
  25. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  26. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  27. package/dist/sdk/src/config.ts +4 -26
  28. package/dist/sdk/src/documents.ts +2 -0
  29. package/dist/sdk/src/hooks/useAction.ts +28 -0
  30. package/dist/sdk/src/hooks/useBackend.ts +3 -2
  31. package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
  32. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  33. package/dist/sdk/src/hooks/useCollectionInteractions.ts +4 -2
  34. package/dist/sdk/src/hooks/useHandover.ts +7 -7
  35. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  36. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  37. package/dist/sdk/src/hooks/useNotis.ts +3 -0
  38. package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
  39. package/dist/sdk/src/hooks/useQuery.ts +7 -4
  40. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  41. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  42. package/dist/sdk/src/index.ts +36 -0
  43. package/dist/sdk/src/interactions/shortcuts.tsx +42 -5
  44. package/dist/sdk/src/nativeData.ts +39 -0
  45. package/dist/sdk/src/presentation.tsx +82 -0
  46. package/dist/sdk/src/provider.tsx +9 -4
  47. package/dist/sdk/src/queryCache.ts +5 -3
  48. package/dist/sdk/src/queryErrors.ts +51 -0
  49. package/dist/sdk/src/runtime.ts +163 -13
  50. package/dist/sdk/src/space.ts +425 -0
  51. package/dist/sdk/src/styles.css +96 -72
  52. package/dist/sdk/src/vite.ts +11 -1
  53. package/dist/skill-sync/index.js +571 -80
  54. package/dist/skill-sync/index.js.map +4 -4
  55. package/dist/skill-sync-worker.mjs +858 -233
  56. package/dist/space-harness/host.js +66 -0
  57. package/dist/view-renderer/dist/host.css +1 -0
  58. package/dist/view-renderer/dist/host.js +1028 -0
  59. package/dist/view-renderer/dist/theme.css +73 -0
  60. package/dist/view-renderer/src/dom.js +180 -0
  61. package/dist/view-renderer/src/errors.js +8 -0
  62. package/dist/view-renderer/src/host.tsx +175 -0
  63. package/dist/view-renderer/src/index.js +4 -0
  64. package/dist/view-renderer/src/pool.js +76 -0
  65. package/dist/view-renderer/src/remote-broker.js +59 -0
  66. package/dist/view-renderer/src/renderer.js +201 -0
  67. package/package.json +16 -5
  68. package/skills/notis-apps/cli.md +323 -163
  69. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +3 -3
  70. package/skills/notis-onboarding/BRIEF.md +7 -3
  71. package/src/command-specs/apps.js +2 -1
  72. package/src/command-specs/auth.js +2 -2
  73. package/src/command-specs/files.js +29 -0
  74. package/src/command-specs/handover.js +331 -79
  75. package/src/command-specs/helpers.js +2 -0
  76. package/src/command-specs/index.js +11 -5
  77. package/src/command-specs/profile.js +42 -1
  78. package/src/command-specs/skills.js +242 -1
  79. package/src/command-specs/spaces.js +645 -0
  80. package/src/command-specs/tools.js +0 -6
  81. package/src/command-specs/views.js +69 -0
  82. package/src/runtime/agent-browser.js +92 -29
  83. package/src/runtime/app-boundary-validator.js +7 -4
  84. package/src/runtime/app-data-literals.js +82 -0
  85. package/src/runtime/app-platform.js +321 -253
  86. package/src/runtime/computer-identity.d.ts +30 -0
  87. package/src/runtime/computer-identity.js +151 -0
  88. package/src/runtime/config-lock.js +135 -0
  89. package/src/runtime/confined-space-build.js +74 -0
  90. package/src/runtime/confined-space-verify.js +49 -0
  91. package/src/runtime/delegated-context.js +5 -21
  92. package/src/runtime/native-record-files.js +47 -0
  93. package/src/runtime/oauth.js +18 -1
  94. package/src/runtime/profiles.js +5 -107
  95. package/src/runtime/retired-sdk-files.js +20 -0
  96. package/src/runtime/screenshot-framing.js +85 -0
  97. package/src/runtime/skill-sync/cloud-client.ts +26 -4
  98. package/src/runtime/skill-sync/index.ts +230 -35
  99. package/src/runtime/skill-sync/local-scanner.ts +188 -29
  100. package/src/runtime/skill-sync/native-identity.ts +102 -0
  101. package/src/runtime/skill-sync/native-plan.ts +70 -0
  102. package/src/runtime/skill-sync/sync-plan.ts +16 -9
  103. package/src/runtime/skill-sync/types.ts +32 -6
  104. package/src/runtime/source-root.js +32 -0
  105. package/src/runtime/space-collection-scaffold.js +66 -0
  106. package/src/runtime/space-database-structures.js +229 -0
  107. package/src/runtime/space-fixture-broker.js +50 -0
  108. package/src/runtime/space-lock.js +337 -0
  109. package/src/runtime/space-platform.js +105 -0
  110. package/src/runtime/space-source.js +281 -0
  111. package/src/runtime/space-test-server.js +327 -0
  112. package/src/runtime/space-verification.js +89 -0
  113. package/src/runtime/space-verify-browser-worker.js +51 -0
  114. package/src/runtime/space-view-manifest.js +254 -0
  115. package/src/runtime/store-screenshot.js +10 -7
  116. package/src/runtime/sync-skills.d.ts +3 -1
  117. package/src/runtime/sync-skills.js +10 -2
  118. package/src/runtime/transport.js +8 -6
  119. package/src/runtime/view-render.js +49 -0
  120. package/src/skill-sync-worker.js +14 -2
  121. package/src/space-harness-host.ts +103 -0
  122. package/template/.harness/index.html.tmpl +7 -10
  123. package/template/app/globals.css +1 -0
  124. package/src/command-specs/reports.js +0 -97
@@ -1,5 +1,7 @@
1
1
  // src/runtime/skill-sync/index.ts
2
- import { createHash as createHash2 } from "node:crypto";
2
+ import { createHash as createHash4 } from "node:crypto";
3
+ import { promises as fs3 } from "node:fs";
4
+ import { dirname, join } from "node:path";
3
5
 
4
6
  // src/runtime/skill-sync/types.ts
5
7
  var DEFAULT_AGENT_TARGETS = {
@@ -17,8 +19,133 @@ function normalizeAgentTargets(targets) {
17
19
  };
18
20
  }
19
21
 
22
+ // src/runtime/skill-sync/native-identity.ts
23
+ import { createHash } from "node:crypto";
24
+ var NATIVE_IDENTITY_FILE = ".notis-native-skill.json";
25
+ var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
26
+ function checkedNativeReference(value, cloudId) {
27
+ const reference = value;
28
+ if (!reference || typeof reference !== "object" || Array.isArray(reference) || Object.keys(reference).sort().join(",") !== "access_revision,kind,skill_id,version" || reference.kind !== "native_standalone_skill" || !UUID.test(reference.skill_id) || cloudId !== void 0 && reference.skill_id !== cloudId || !Number.isSafeInteger(reference.access_revision) || reference.access_revision < 0 || !reference.version || Object.keys(reference.version).sort().join(",") !== "digest,revision" || !Number.isSafeInteger(reference.version.revision) || reference.version.revision < 1 || !/^[a-f0-9]{64}$/.test(reference.version.digest)) {
29
+ throw new Error("Native Skill identity is unavailable. Refresh before syncing.");
30
+ }
31
+ return structuredClone(reference);
32
+ }
33
+ function nativeDirectory(reference) {
34
+ return "native-" + checkedNativeReference(reference).skill_id;
35
+ }
36
+ function sameNativeReference(left, right) {
37
+ return Boolean(left && right && left.skill_id === right.skill_id && left.access_revision === right.access_revision && left.version.revision === right.version.revision && left.version.digest === right.version.digest);
38
+ }
39
+ function isNativeSkill(skill) {
40
+ return skill.source === "native" || skill.native_reference !== void 0;
41
+ }
42
+ function assertNativePull(response) {
43
+ const ids = /* @__PURE__ */ new Set();
44
+ for (const skill of response.skills) {
45
+ if (!isNativeSkill(skill)) continue;
46
+ const reference = checkedNativeReference(skill.native_reference, skill.id);
47
+ if (response.skill_sync_protocol !== 2 || skill.name !== nativeDirectory(reference) || ids.has(skill.id) || !/^[a-f0-9]{64}$/.test(response.sync_scope || "") || skill.native_scope !== response.sync_scope || !Array.isArray(skill.bundle_files) || skill.bundle_hydration_failed || typeof skill.skill_md !== "string") {
48
+ throw new Error("Native Skill inventory is incomplete. Existing local files were retained.");
49
+ }
50
+ const paths = /* @__PURE__ */ new Set();
51
+ let entrypoint = false;
52
+ for (const file of skill.bundle_files) {
53
+ if (typeof file.path !== "string" || typeof file.content_b64 !== "string" || file.path.startsWith("/") || file.path.includes("\\") || file.path.includes("\0") || file.path.split("/").some((part) => !part || part === "." || part === "..") || paths.has(file.path.toLowerCase()) || file.path.toLowerCase() === NATIVE_IDENTITY_FILE) {
54
+ throw new Error("Native Skill bundle has a conflicting managed path.");
55
+ }
56
+ paths.add(file.path.toLowerCase());
57
+ if (file.path === "SKILL.md") {
58
+ entrypoint = Buffer.from(file.content_b64, "base64").equals(Buffer.from(skill.skill_md, "utf8"));
59
+ }
60
+ }
61
+ if (!entrypoint) throw new Error("Native Skill instructions and files do not match.");
62
+ ids.add(skill.id);
63
+ }
64
+ }
65
+ function nativeFileDigest(bytes) {
66
+ return createHash("sha256").update(bytes).digest("hex");
67
+ }
68
+ function nativeFrontmatterName(markdown) {
69
+ const header = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
70
+ return header?.[1].match(/^name\s*:\s*(.+)$/m)?.[1].trim().replace(/^['"]|['"]$/g, "") || "";
71
+ }
72
+ function checkedNativeIdentity(value) {
73
+ const identity = value;
74
+ if (!identity || identity.schema !== 1 || typeof identity.cloudId !== "string" || typeof identity.displayName !== "string" || typeof identity.frontmatterName !== "string" || !/^[a-f0-9]{64}$/.test(identity.scope) || !/^[a-f0-9]{64}$/.test(identity.folderHash) || !identity.files || !Object.keys(identity.files).includes("SKILL.md") || Object.entries(identity.files).some(([name, hash]) => name.startsWith("/") || name.includes("\\") || name.split("/").some((part) => !part || part === "." || part === "..") || !/^[a-f0-9]{64}$/.test(hash))) {
75
+ throw new Error("Native Skill provenance is incomplete. Existing files were retained.");
76
+ }
77
+ return { ...identity, reference: checkedNativeReference(identity.reference, identity.cloudId) };
78
+ }
79
+ function nativeState(skill) {
80
+ if (!isNativeSkill(skill)) return {};
81
+ return {
82
+ nativeReference: checkedNativeReference(skill.native_reference, skill.id),
83
+ ...skill.native_settings ? { nativeSettingsRevision: skill.native_settings.revision } : {},
84
+ nativeDisplayName: skill.display_name || skill.name,
85
+ nativeScope: skill.native_scope,
86
+ nativeUnavailable: false
87
+ };
88
+ }
89
+
90
+ // src/runtime/skill-sync/native-plan.ts
91
+ function bindNativeFolders(response, locals, state) {
92
+ const nativeIds = new Set(response.skills.filter(isNativeSkill).map((skill) => skill.id));
93
+ for (const entry of Object.values(state.skills)) if (entry.nativeReference || entry.nativeUnavailable) nativeIds.add(entry.cloudId);
94
+ const byId = /* @__PURE__ */ new Map();
95
+ for (const local of locals) {
96
+ const id = local.cloudId || state.skills[local.name]?.cloudId;
97
+ if (!id || !nativeIds.has(id) && !local.nativeReference) continue;
98
+ if (local.nativeReference) checkedNativeReference(local.nativeReference, id);
99
+ if (byId.has(id)) throw new Error("Two local folders claim the same native Skill. Keep both copies and resolve the conflict.");
100
+ byId.set(id, local);
101
+ }
102
+ const skills = response.skills.map((skill) => isNativeSkill(skill) && byId.has(skill.id) ? { ...skill, name: byId.get(skill.id).name } : skill);
103
+ for (const skill of skills.filter(isNativeSkill)) {
104
+ if (skills.some((other) => other.id !== skill.id && other.name === skill.name)) {
105
+ throw new Error("A native Skill and another Skill share a local folder. Existing files were retained.");
106
+ }
107
+ }
108
+ return { ...response, skills };
109
+ }
110
+ function previousNativeState(state, skill) {
111
+ const named = state.skills[skill.name];
112
+ return named?.cloudId === skill.id ? named : Object.values(state.skills).find((entry) => entry.cloudId === skill.id);
113
+ }
114
+ function nativeLocalReady(skill, local) {
115
+ return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash && local.cloudId === skill.id && local.nativeScope === skill.native_scope && sameNativeReference(local.nativeReference, skill.native_reference));
116
+ }
117
+ function pendingLegacyUpgrade(skill, local, state) {
118
+ if (!isNativeSkill(skill) || !local || local.nativeReference || local.cloudId) return false;
119
+ const previous = previousNativeState(state, skill);
120
+ return Boolean(previous && !previous.nativeReference);
121
+ }
122
+ function nativeLinkRows(response, locals, state) {
123
+ return response.skills.map((skill) => {
124
+ const local = locals.find((row) => row.name === skill.name);
125
+ return isNativeSkill(skill) && !nativeLocalReady(skill, local) && !(state && pendingLegacyUpgrade(skill, local, state)) ? { ...skill, status: "disabled" } : skill;
126
+ });
127
+ }
128
+ function retainMissingNativeState(previous, next) {
129
+ const present = new Set(Object.values(next.skills).map((row) => row.cloudId));
130
+ const retained = Object.fromEntries(Object.entries(previous.skills).filter(([, row]) => (row.nativeReference || row.nativeUnavailable) && !present.has(row.cloudId)).map(([name, row]) => [name, { ...row, nativeUnavailable: true, verifiedAgentLinks: {} }]));
131
+ const history = structuredClone(previous.nativeHistory || {});
132
+ for (const [name, row] of [...Object.entries(previous.skills), ...Object.entries(next.skills)]) {
133
+ if (!row.nativeReference && !row.nativeUnavailable) continue;
134
+ const names = history[row.cloudId]?.names || [];
135
+ history[row.cloudId] = { names: [.../* @__PURE__ */ new Set([...names, name])] };
136
+ }
137
+ return {
138
+ ...next,
139
+ skills: { ...retained, ...next.skills },
140
+ ...Object.keys(history).length ? { nativeHistory: history } : {}
141
+ };
142
+ }
143
+ function hasNativeFolderHistory(state, name) {
144
+ return Object.values(state.nativeHistory || {}).some((row) => row.names.includes(name));
145
+ }
146
+
20
147
  // src/runtime/skill-sync/local-scanner.ts
21
- import { createHash } from "crypto";
148
+ import { createHash as createHash2 } from "crypto";
22
149
  import { execFile } from "child_process";
23
150
  import { promises as fs } from "fs";
24
151
  import os from "os";
@@ -26,6 +153,8 @@ import path from "path";
26
153
  import { promisify } from "util";
27
154
  var HOME_DIR = os.homedir();
28
155
  var execFileAsync = promisify(execFile);
156
+ var NativeSkillIdentityError = class extends Error {
157
+ };
29
158
  var AGENTS_DIR = path.join(HOME_DIR, ".agents");
30
159
  var LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
31
160
  var NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
@@ -85,15 +214,18 @@ function sanitizePathSegment(value) {
85
214
  if (sanitized === raw) {
86
215
  return sanitized;
87
216
  }
88
- return `${sanitized}-${createHash("sha256").update(raw).digest("hex").slice(0, 12)}`;
217
+ return `${sanitized}-${createHash2("sha256").update(raw).digest("hex").slice(0, 12)}`;
89
218
  }
90
- function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR) {
219
+ function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR, namespace, syncHome) {
91
220
  const safeUserId = sanitizePathSegment(authUserId);
92
- const resolvedAgentsDir = path.resolve(agentsDir);
93
- const syncRoot = getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
94
- const userRoot = path.join(syncRoot, "users", safeUserId);
221
+ const homeDir = syncHome ? path.resolve(syncHome) : void 0;
222
+ const resolvedAgentsDir = homeDir ? path.join(homeDir, ".agents") : path.resolve(agentsDir);
223
+ const syncRoot = homeDir ? path.join(homeDir, ".notis", "skills") : getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
224
+ const userRoot = namespace ? path.join(syncRoot, "environments", sanitizePathSegment(namespace), "users", safeUserId) : path.join(syncRoot, "users", safeUserId);
95
225
  const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
96
226
  return {
227
+ ...homeDir ? { homeDir } : {},
228
+ ...namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {},
97
229
  agentsDir: resolvedAgentsDir,
98
230
  syncRoot,
99
231
  legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
@@ -142,16 +274,23 @@ async function readJsonFile(filePath) {
142
274
  return null;
143
275
  }
144
276
  }
145
- async function listFilesRecursive(dirPath) {
277
+ var IGNORED_SKILL_DIRECTORIES = /* @__PURE__ */ new Set(["__pycache__", ".pytest_cache"]);
278
+ function isIgnoredSkillEntry(entry) {
279
+ if (entry.name === ".DS_Store") return true;
280
+ if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
281
+ return /\.py[co]$/i.test(entry.name);
282
+ }
283
+ async function listFilesRecursive(dirPath, rootPath = dirPath, strictLinks = false) {
146
284
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
147
285
  const nested = await Promise.all(
148
286
  entries.map(async (entry) => {
149
- if (entry.name === ".DS_Store") {
287
+ if (isIgnoredSkillEntry(entry) || dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE) {
150
288
  return [];
151
289
  }
152
290
  const fullPath = path.join(dirPath, entry.name);
291
+ if (strictLinks && entry.isSymbolicLink()) throw new Error("Native Skill bundles cannot contain symbolic links.");
153
292
  if (entry.isDirectory()) {
154
- return listFilesRecursive(fullPath);
293
+ return listFilesRecursive(fullPath, rootPath, strictLinks);
155
294
  }
156
295
  if (entry.isFile()) {
157
296
  return [fullPath];
@@ -162,7 +301,7 @@ async function listFilesRecursive(dirPath) {
162
301
  return nested.flat().sort();
163
302
  }
164
303
  async function computeFolderHash(dirPath) {
165
- const hash = createHash("sha256");
304
+ const hash = createHash2("sha256");
166
305
  const filePaths = await listFilesRecursive(dirPath);
167
306
  for (const filePath of filePaths) {
168
307
  const relativePath = path.relative(dirPath, filePath);
@@ -240,17 +379,17 @@ function defaultTopLevelSkillSources(paths) {
240
379
  { label: "agents", root: paths.legacySkillsDir, priority: 2 },
241
380
  {
242
381
  label: "codex",
243
- root: path.join(HOME_DIR, ".codex", "skills"),
382
+ root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
244
383
  priority: 3
245
384
  },
246
385
  {
247
386
  label: "cursor",
248
- root: path.join(HOME_DIR, ".cursor", "skills"),
387
+ root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
249
388
  priority: 4
250
389
  },
251
390
  {
252
391
  label: "claude",
253
- root: path.join(HOME_DIR, ".claude", "skills"),
392
+ root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
254
393
  priority: 5
255
394
  }
256
395
  );
@@ -470,6 +609,7 @@ async function ensureCanonicalSkillsDir(paths = DEFAULT_SYNC_PATHS) {
470
609
  async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
471
610
  await ensureCanonicalSkillsDir(paths);
472
611
  const sourceUrls = await readSkillLock(paths);
612
+ const previousState = await readSyncState(paths);
473
613
  const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
474
614
  const skills = await Promise.all(
475
615
  entries.map(async (entry) => {
@@ -478,6 +618,15 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
478
618
  }
479
619
  const skillDir = path.join(paths.skillsDir, entry.name);
480
620
  const skillMdPath = path.join(skillDir, "SKILL.md");
621
+ let marker = null;
622
+ try {
623
+ marker = await fs.readFile(path.join(skillDir, NATIVE_IDENTITY_FILE), "utf8");
624
+ } catch (error) {
625
+ if (error.code !== "ENOENT") {
626
+ throw new NativeSkillIdentityError("Native Skill provenance could not be read; existing files were retained.");
627
+ }
628
+ }
629
+ const knownNative = marker !== null || Boolean(previousState.skills[entry.name]?.nativeReference) || Object.values(previousState.nativeHistory || {}).some((row) => row.names.includes(entry.name));
481
630
  try {
482
631
  const skillMd = await fs.readFile(skillMdPath, "utf8");
483
632
  const { description } = parseFrontMatter(skillMd);
@@ -489,11 +638,38 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
489
638
  folderHash,
490
639
  directoryPath: skillDir
491
640
  };
641
+ if (marker !== null) {
642
+ let identity;
643
+ try {
644
+ identity = checkedNativeIdentity(JSON.parse(marker));
645
+ } catch {
646
+ throw new NativeSkillIdentityError("Native Skill provenance is unreadable; do not upload it as a new Skill.");
647
+ }
648
+ let current = true;
649
+ for (const [name, expected] of Object.entries(identity.files)) {
650
+ try {
651
+ if (nativeFileDigest(await fs.readFile(resolveSkillBundleFilePath(skillDir, name))) !== expected) current = false;
652
+ } catch {
653
+ current = false;
654
+ }
655
+ }
656
+ const editedName = nativeFrontmatterName(skillMd);
657
+ Object.assign(skill, {
658
+ cloudId: identity.cloudId,
659
+ nativeReference: identity.reference,
660
+ nativeScope: identity.scope,
661
+ nativeBaseFolderHash: identity.folderHash,
662
+ nativeFilesCurrent: current,
663
+ displayName: editedName && editedName !== identity.frontmatterName ? editedName : identity.displayName
664
+ });
665
+ }
492
666
  if (sourceUrls[entry.name]) {
493
667
  skill.sourceUrl = sourceUrls[entry.name];
494
668
  }
495
669
  return skill;
496
- } catch {
670
+ } catch (error) {
671
+ if (error instanceof NativeSkillIdentityError) throw error;
672
+ if (knownNative) throw new NativeSkillIdentityError("Native Skill files could not be read; existing files were retained.");
497
673
  return null;
498
674
  }
499
675
  })
@@ -504,6 +680,9 @@ function normalizeSyncState(state) {
504
680
  if (!state || state.version !== 1 || typeof state.skills !== "object") {
505
681
  return null;
506
682
  }
683
+ if (state.nativeHistory && (typeof state.nativeHistory !== "object" || Array.isArray(state.nativeHistory) || Object.values(state.nativeHistory).some((row) => !row || !Array.isArray(row.names) || row.names.some((name) => typeof name !== "string")))) {
684
+ throw new Error("Native Skill sync provenance is unreadable; existing files were retained.");
685
+ }
507
686
  const normalizeStoredAgentTargets = (targets) => ({
508
687
  notis: Boolean(targets?.notis ?? true),
509
688
  claude_code: Boolean(targets?.claude_code ?? true),
@@ -512,6 +691,7 @@ function normalizeSyncState(state) {
512
691
  });
513
692
  return {
514
693
  version: 1,
694
+ ...state.nativeHistory ? { nativeHistory: state.nativeHistory } : {},
515
695
  lastSyncedAt: typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
516
696
  skills: Object.fromEntries(
517
697
  Object.entries(state.skills || {}).map(([skillName, skillState]) => [
@@ -586,20 +766,49 @@ async function createZipFromDirectory(directoryPath) {
586
766
  os.tmpdir(),
587
767
  `notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
588
768
  );
589
- const parentDir = path.dirname(directoryPath);
590
769
  const directoryName = path.basename(directoryPath);
591
- if (process.platform === "win32") {
592
- await execFileAsync("powershell.exe", [
593
- "-NoProfile",
594
- "-Command",
595
- `Compress-Archive -Path '${directoryPath.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`
596
- ]);
770
+ const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "notis-skill-bundle-"));
771
+ try {
772
+ const stagedDir = path.join(stagingRoot, directoryName);
773
+ for (const filePath of await listFilesRecursive(directoryPath)) {
774
+ const target = path.join(stagedDir, path.relative(directoryPath, filePath));
775
+ await fs.mkdir(path.dirname(target), { recursive: true });
776
+ await fs.copyFile(filePath, target);
777
+ }
778
+ await fs.mkdir(stagedDir, { recursive: true });
779
+ if (process.platform === "win32") {
780
+ const systemTar = path.join(
781
+ process.env.SystemRoot || "C:\\Windows",
782
+ "System32",
783
+ "tar.exe"
784
+ );
785
+ if (await pathExists(systemTar)) {
786
+ await execFileAsync(systemTar, [
787
+ "-c",
788
+ "--format",
789
+ "zip",
790
+ "-f",
791
+ zipPath,
792
+ "-C",
793
+ stagingRoot,
794
+ directoryName
795
+ ]);
796
+ return zipPath;
797
+ }
798
+ await execFileAsync("powershell.exe", [
799
+ "-NoProfile",
800
+ "-Command",
801
+ `Compress-Archive -Path '${stagedDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`
802
+ ]);
803
+ return zipPath;
804
+ }
805
+ await execFileAsync("zip", ["-qry", zipPath, directoryName], {
806
+ cwd: stagingRoot
807
+ });
597
808
  return zipPath;
809
+ } finally {
810
+ await fs.rm(stagingRoot, { recursive: true, force: true });
598
811
  }
599
- await execFileAsync("zip", ["-qry", zipPath, directoryName], {
600
- cwd: parentDir
601
- });
602
- return zipPath;
603
812
  }
604
813
  async function extractZipToDirectory(zipPath, destinationDir) {
605
814
  const extractRoot = await fs.mkdtemp(
@@ -642,13 +851,18 @@ async function listRelativeFiles(dirPath) {
642
851
  function toPosixRelativePath(rootDir, filePath) {
643
852
  return path.relative(rootDir, filePath).split(path.sep).join("/");
644
853
  }
645
- async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied) {
854
+ async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied, strictConflicts = false) {
646
855
  const managed = previouslyApplied ? new Set(previouslyApplied) : null;
647
856
  for (const existingFile of await listFilesRecursive(existingDir)) {
648
857
  const relativePath = toPosixRelativePath(existingDir, existingFile);
649
858
  if (managed?.has(relativePath)) continue;
650
859
  const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
651
- if (await pathExists(stagedPath)) continue;
860
+ if (await pathExists(stagedPath)) {
861
+ if (strictConflicts && !(await fs.readFile(existingFile)).equals(await fs.readFile(stagedPath))) {
862
+ throw new Error("An incoming native Skill file conflicts with a local file. Local files were retained.");
863
+ }
864
+ continue;
865
+ }
652
866
  await fs.mkdir(path.dirname(stagedPath), { recursive: true });
653
867
  await fs.copyFile(existingFile, stagedPath);
654
868
  }
@@ -676,13 +890,46 @@ async function replaceSkillDirectoryAtomically(skillDir, populateDir, preserve)
676
890
  let promotedStaging = false;
677
891
  let cleanupError = null;
678
892
  try {
893
+ if (preserve?.native && preserve.expectedFolderHash === void 0) {
894
+ let existing = false;
895
+ try {
896
+ await fs.lstat(skillDir);
897
+ existing = true;
898
+ } catch (error) {
899
+ if (error.code !== "ENOENT") throw error;
900
+ }
901
+ if (existing) throw new Error("Existing native Skill files need a readable baseline before replacement.");
902
+ }
903
+ if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
904
+ throw new Error("Local Skill files changed before sync; the newer local edits were retained.");
905
+ }
679
906
  await populateDir(stagingDir);
680
907
  if (preserve && await pathExists(skillDir)) {
681
- await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied);
908
+ await preserveUnmanagedFiles(
909
+ skillDir,
910
+ stagingDir,
911
+ preserve.previouslyApplied,
912
+ preserve.native && !preserve.legacyBaseline
913
+ );
914
+ }
915
+ if (preserve?.native) {
916
+ const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
917
+ const marker = checkedNativeIdentity(JSON.parse(await fs.readFile(markerPath, "utf8")));
918
+ marker.folderHash = await computeFolderHash(stagingDir);
919
+ await fs.writeFile(markerPath, JSON.stringify(marker), { mode: 384 });
920
+ }
921
+ if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
922
+ throw new Error("Local Skill files changed during sync; the newer local edits were retained.");
682
923
  }
683
924
  if (await pathExists(skillDir)) {
684
925
  await fs.rename(skillDir, backupDir);
685
926
  movedExisting = true;
927
+ if (preserve?.native && preserve.expectedFolderHash === void 0) {
928
+ throw new Error("Native Skill files appeared during sync; existing files were retained.");
929
+ }
930
+ if (preserve?.expectedFolderHash !== void 0 && await computeFolderHash(backupDir) !== preserve.expectedFolderHash) {
931
+ throw new Error("Local Skill files changed before publication; the newer local edits were retained.");
932
+ }
686
933
  }
687
934
  await fs.rename(stagingDir, skillDir);
688
935
  promotedStaging = true;
@@ -727,12 +974,43 @@ async function createSkillBundleBase64(skill) {
727
974
  await fs.rm(zipPath, { force: true });
728
975
  }
729
976
  }
977
+ async function captureNativeSkillFiles(skill) {
978
+ const hash = createHash2("sha256");
979
+ const files = [];
980
+ for (const filePath of await listFilesRecursive(skill.directoryPath, skill.directoryPath, true)) {
981
+ const relative = path.relative(skill.directoryPath, filePath);
982
+ const bytes = await fs.readFile(filePath);
983
+ hash.update(relative);
984
+ hash.update("\0");
985
+ hash.update(bytes);
986
+ hash.update("\0");
987
+ files.push({ path: relative.split(path.sep).join("/"), content_b64: bytes.toString("base64") });
988
+ }
989
+ if (hash.digest("hex") !== skill.folderHash) throw new Error("Local Skill files changed before upload. Sync again with the current files.");
990
+ return files;
991
+ }
730
992
  async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PATHS, options = {}) {
993
+ if (isNativeSkill(skill)) {
994
+ assertNativePull({
995
+ skills: [{ ...skill, name: "native-" + skill.id }],
996
+ skill_sync_protocol: 2,
997
+ sync_scope: skill.native_scope,
998
+ sync_enabled: true,
999
+ last_synced_at: null
1000
+ });
1001
+ if (bundleBytes?.length) throw new Error("Native Skills require their verified complete file response.");
1002
+ }
731
1003
  const skillDir = path.join(
732
1004
  paths.skillsDir,
733
1005
  safeName(skill.name, paths.skillsDir)
734
1006
  );
735
- const preserve = { previouslyApplied: options.previouslyApplied };
1007
+ const preserve = {
1008
+ previouslyApplied: options.previouslyApplied,
1009
+ expectedFolderHash: options.expectedFolderHash,
1010
+ native: isNativeSkill(skill),
1011
+ // An unchanged legacy folder upgrading in place: shared files take the cloud version.
1012
+ legacyBaseline: isNativeSkill(skill) && options.legacyBaseline === true && options.expectedFolderHash !== void 0
1013
+ };
736
1014
  if (bundleBytes?.length) {
737
1015
  const bundlePath = path.join(
738
1016
  os.tmpdir(),
@@ -772,6 +1050,20 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
772
1050
  );
773
1051
  appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
774
1052
  }
1053
+ if (skill.native_reference) {
1054
+ const identity = {
1055
+ schema: 1,
1056
+ cloudId: skill.id,
1057
+ reference: skill.native_reference,
1058
+ displayName: skill.display_name || skill.name,
1059
+ frontmatterName: nativeFrontmatterName(skill.skill_md || ""),
1060
+ scope: skill.native_scope,
1061
+ folderHash: await computeFolderHash(stagingDir),
1062
+ files: Object.fromEntries((skill.bundle_files || []).map((file) => [file.path, nativeFileDigest(Buffer.from(file.content_b64, "base64"))]))
1063
+ };
1064
+ await fs.writeFile(path.join(stagingDir, NATIVE_IDENTITY_FILE), JSON.stringify(identity), { mode: 384 });
1065
+ appliedFiles.push(NATIVE_IDENTITY_FILE);
1066
+ }
775
1067
  }, preserve);
776
1068
  return { written: true, appliedFiles: appliedFiles.sort() };
777
1069
  }
@@ -786,6 +1078,7 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
786
1078
  }
787
1079
 
788
1080
  // src/runtime/skill-sync/cloud-client.ts
1081
+ import { createHash as createHash3 } from "node:crypto";
789
1082
  async function requestJson(url, jwt, options = {}) {
790
1083
  const response = await fetch(url, {
791
1084
  signal: AbortSignal.timeout(9e4),
@@ -809,7 +1102,7 @@ async function fetchSyncSettings(serverUrl, jwt) {
809
1102
  }
810
1103
  async function pullSkills(serverUrl, jwt) {
811
1104
  return requestJson(`${serverUrl}/portal_skills/sync-pull`, jwt, {
812
- body: {}
1105
+ body: { skill_sync_protocol: 2 }
813
1106
  });
814
1107
  }
815
1108
  async function pushChangedSkills(serverUrl, jwt, changedSkills) {
@@ -819,10 +1112,17 @@ async function pushChangedSkills(serverUrl, jwt, changedSkills) {
819
1112
  skill_md: skill.skillMd,
820
1113
  source_url: skill.sourceUrl,
821
1114
  folder_hash: skill.folderHash,
822
- bundle_base64: await createSkillBundleBase64(skill)
1115
+ ...skill.nativeReference ? { bundle_files: await captureNativeSkillFiles(skill) } : { bundle_base64: await createSkillBundleBase64(skill) },
1116
+ ...skill.cloudId ? { cloud_id: skill.cloudId } : {},
1117
+ ...skill.nativeReference ? {
1118
+ native_reference: skill.nativeReference,
1119
+ display_name: skill.displayName,
1120
+ native_scope: skill.nativeScope
1121
+ } : {}
823
1122
  })));
824
1123
  return requestJson(`${serverUrl}/portal_skills/sync-push`, jwt, {
825
1124
  body: {
1125
+ skill_sync_protocol: 2,
826
1126
  skills: payloadSkills
827
1127
  }
828
1128
  });
@@ -835,7 +1135,26 @@ async function downloadSkillBundle(bundleUrl) {
835
1135
  }
836
1136
  return Buffer.from(await response.arrayBuffer());
837
1137
  }
838
- async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt) {
1138
+ async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt, native) {
1139
+ if (native) {
1140
+ checkedNativeReference(native.reference, skillId);
1141
+ if (!Number.isSafeInteger(native.settingsRevision) || native.settingsRevision < 0) throw new Error("Refresh native Skill settings before changing assignments.");
1142
+ const payload = {
1143
+ operation: "update",
1144
+ target: { skill_id: skillId, access_revision: native.reference.access_revision },
1145
+ revision: native.settingsRevision,
1146
+ patch: { agent_targets: Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b))) }
1147
+ };
1148
+ const requestId = "sync-settings-" + createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
1149
+ const saved = await requestJson(
1150
+ `${serverUrl}/portal_skills/native-settings`,
1151
+ jwt,
1152
+ { body: { ...payload, request_id: requestId } }
1153
+ );
1154
+ const current = saved.current || saved;
1155
+ if (saved.skill_id !== skillId || typeof saved.receipt_id !== "string" || !saved.receipt_id || current.target?.skill_id !== skillId || !current.settings) throw new Error("Native assignment update was not confirmed.");
1156
+ return { success: true, agent_targets: current.settings.agent_targets, updated_at: current.settings.updated_at };
1157
+ }
839
1158
  return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
840
1159
  method: "PATCH",
841
1160
  body: {
@@ -1161,22 +1480,27 @@ function selectCloudSkillsToApply(skills) {
1161
1480
  }
1162
1481
  return [...winners.values()];
1163
1482
  }
1164
- function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudAppOwnedSkillNames = /* @__PURE__ */ new Set()) {
1483
+ function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudSkills = []) {
1165
1484
  return localSkills.filter((skill) => {
1166
1485
  if (cloudCuratedSkillNames.has(skill.name)) {
1167
1486
  return false;
1168
1487
  }
1169
- if (cloudAppOwnedSkillNames.has(skill.name)) {
1170
- return false;
1488
+ const previous = syncState.skills[skill.name] || (skill.cloudId ? Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId) : void 0);
1489
+ const native = cloudSkills.find((row) => isNativeSkill(row) && row.id === (skill.cloudId || previous?.cloudId));
1490
+ if (native || skill.nativeReference || previous?.nativeReference || previous?.nativeUnavailable) {
1491
+ return Boolean(native && skill.nativeReference && skill.nativeScope === native.native_scope && (previous ? previous.folderHash !== skill.folderHash : skill.nativeBaseFolderHash !== skill.folderHash));
1171
1492
  }
1172
- const previous = syncState.skills[skill.name];
1173
1493
  if (!previous) {
1494
+ if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
1174
1495
  return true;
1175
1496
  }
1176
1497
  if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
1177
1498
  return false;
1178
1499
  }
1179
1500
  return previous.folderHash !== skill.folderHash;
1501
+ }).map((skill) => {
1502
+ const previous = syncState.skills[skill.name] || Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId);
1503
+ return { ...skill, ...skill.cloudId || previous?.cloudId ? { cloudId: skill.cloudId || previous.cloudId } : {} };
1180
1504
  });
1181
1505
  }
1182
1506
 
@@ -1252,6 +1576,62 @@ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
1252
1576
  removeForeignAccountSymlinks,
1253
1577
  updateAgentTargets
1254
1578
  };
1579
+ var MIRROR_ONLY_DEPS = {
1580
+ removeForeignAccountSymlinks: async () => 0,
1581
+ removeAllSymlinksForSkill: async () => 0,
1582
+ syncSymlinks: async () => ({ linked: 0, removed: 0, skipped: 0, verifiedAgentLinks: {} })
1583
+ };
1584
+ async function forgetOtherAccountsAgentLinks(paths) {
1585
+ const usersDir = dirname(dirname(paths.syncStatePath));
1586
+ let entries;
1587
+ try {
1588
+ entries = await fs3.readdir(usersDir, { withFileTypes: true });
1589
+ } catch {
1590
+ return;
1591
+ }
1592
+ for (const entry of entries) {
1593
+ if (!entry.isDirectory()) continue;
1594
+ const statePath = join(usersDir, entry.name, ".notis-sync.json");
1595
+ if (statePath === paths.syncStatePath) continue;
1596
+ let state = null;
1597
+ try {
1598
+ state = JSON.parse(await fs3.readFile(statePath, "utf8"));
1599
+ } catch {
1600
+ continue;
1601
+ }
1602
+ if (!state || typeof state !== "object" || !state.skills || typeof state.skills !== "object") continue;
1603
+ let changed = false;
1604
+ for (const skill of Object.values(state.skills)) {
1605
+ if (skill && typeof skill === "object" && "verifiedAgentLinks" in skill) {
1606
+ delete skill.verifiedAgentLinks;
1607
+ changed = true;
1608
+ }
1609
+ }
1610
+ if (!changed) continue;
1611
+ const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
1612
+ await fs3.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
1613
+ `, "utf8");
1614
+ await fs3.rename(temporaryPath, statePath);
1615
+ }
1616
+ }
1617
+ function isolatedLinkOptions(paths) {
1618
+ if (!paths.isolatedNamespace && !paths.homeDir) return void 0;
1619
+ const root = paths.isolatedNamespace ? join(paths.syncRoot, "environments", paths.isolatedNamespace, "agents") : paths.homeDir;
1620
+ return {
1621
+ agentSkillDirs: {
1622
+ notis: paths.skillsDir,
1623
+ claude_code: join(root, ".claude", "skills"),
1624
+ cursor: join(root, ".cursor", "skills"),
1625
+ codex: join(root, ".codex", "skills")
1626
+ },
1627
+ legacyGlobalSkillsDir: join(root, ".agents", "skills")
1628
+ };
1629
+ }
1630
+ function assertSyncEnvironment(pull, settings) {
1631
+ if ((pull.sync_namespace || null) !== (settings.sync_namespace || null) || settings.sync_scope !== void 0 && pull.sync_scope !== settings.sync_scope) {
1632
+ throw new Error("Skill sync environment changed. Existing mirrors were retained.");
1633
+ }
1634
+ }
1255
1635
  function toSkillMap(skills) {
1256
1636
  return new Map(skills.map((skill) => [skill.name, skill]));
1257
1637
  }
@@ -1268,7 +1648,7 @@ function decodeJwtSubject(jwt) {
1268
1648
  }
1269
1649
  }
1270
1650
  function cloudContentHash(skill) {
1271
- return createHash2("sha256").update(JSON.stringify({
1651
+ return createHash4("sha256").update(JSON.stringify({
1272
1652
  md: skill.skill_md,
1273
1653
  hash: skill.skill_folder_hash,
1274
1654
  source: skill.skill_source_url,
@@ -1283,6 +1663,12 @@ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
1283
1663
  if (!localSkill) {
1284
1664
  return true;
1285
1665
  }
1666
+ if (isNativeSkill(cloudSkill)) {
1667
+ if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
1668
+ if (nativeLocalReady(cloudSkill, localSkill)) return false;
1669
+ const nativePrevious = previousNativeState(previousState, cloudSkill);
1670
+ return localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash || !localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id && nativePrevious.folderHash === localSkill.folderHash;
1671
+ }
1286
1672
  const previous = previousState.skills[skillName];
1287
1673
  if (previous?.folderHash === localSkill.folderHash && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
1288
1674
  if (previous && previous.cloudContentHash === void 0 && previous.cloudId === cloudSkill.id && previous.appliedCloudFolderHash !== void 0 && previous.appliedCloudFolderHash === cloudHash && previous.cloudUpdatedAt === cloudSkill.updated_at && previous.folderHash === localSkill.folderHash) {
@@ -1308,17 +1694,32 @@ function collectAppliedFiles(pullResponse, previousState, appliedFilesByName) {
1308
1694
  }
1309
1695
  return applied;
1310
1696
  }
1311
- function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}) {
1697
+ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}, previousState = { version: 1, lastSyncedAt: null, skills: {} }) {
1312
1698
  const localSkillMap = toSkillMap(localSkills);
1313
1699
  const skills = Object.fromEntries(
1314
1700
  selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
1315
1701
  const localSkill = localSkillMap.get(skill.name);
1316
1702
  const appliedCloudFolderHash = appliedRevisions[skill.name];
1317
1703
  const appliedFileList = appliedFiles[skill.name];
1704
+ if (pendingLegacyUpgrade(skill, localSkill, previousState)) {
1705
+ const { nativeUnavailable: _unavailable, ...previous } = previousNativeState(previousState, skill);
1706
+ return [skill.name, { ...previous, verifiedAgentLinks: verifiedAgentLinks[skill.name] ?? {} }];
1707
+ }
1708
+ if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
1709
+ const previous = previousNativeState(previousState, skill);
1710
+ return [skill.name, { ...previous || {
1711
+ cloudId: skill.id,
1712
+ folderHash: localSkill?.nativeBaseFolderHash || "",
1713
+ agentTargets: normalizeAgentTargets(skill.agent_targets),
1714
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString(),
1715
+ ...localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}
1716
+ }, nativeUnavailable: true, verifiedAgentLinks: {} }];
1717
+ }
1318
1718
  return [
1319
1719
  skill.name,
1320
1720
  {
1321
1721
  cloudId: skill.id,
1722
+ ...nativeState(skill),
1322
1723
  folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
1323
1724
  agentTargets: normalizeAgentTargets(skill.agent_targets),
1324
1725
  verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
@@ -1331,17 +1732,30 @@ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLi
1331
1732
  ];
1332
1733
  })
1333
1734
  );
1334
- return {
1735
+ for (const local of localSkills) {
1736
+ if (!local.nativeReference || Object.values(skills).some((row) => row.cloudId === local.cloudId)) continue;
1737
+ skills[local.name] = {
1738
+ cloudId: local.cloudId,
1739
+ nativeReference: local.nativeReference,
1740
+ nativeScope: local.nativeScope,
1741
+ nativeUnavailable: true,
1742
+ verifiedAgentLinks: {},
1743
+ folderHash: local.nativeBaseFolderHash || "",
1744
+ agentTargets: normalizeAgentTargets(),
1745
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
1746
+ };
1747
+ }
1748
+ return retainMissingNativeState(previousState, {
1335
1749
  version: 1,
1336
1750
  lastSyncedAt,
1337
1751
  skills
1338
- };
1752
+ });
1339
1753
  }
1340
1754
  function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1341
1755
  const cloudSkillNames = new Set(
1342
1756
  pullResponse.skills.map((skill) => skill.name)
1343
1757
  );
1344
- const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).map((skill) => {
1758
+ const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).filter((skill) => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference && !previousState.skills[skill.name]?.nativeUnavailable && !hasNativeFolderHistory(previousState, skill.name) && !/^native-[a-f0-9-]{36}$/.test(skill.name)).map((skill) => {
1345
1759
  const previous = previousState.skills[skill.name];
1346
1760
  return {
1347
1761
  id: previous?.cloudId || `local-${skill.name}`,
@@ -1354,7 +1768,7 @@ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1354
1768
  status: "active"
1355
1769
  };
1356
1770
  });
1357
- return [...pullResponse.skills, ...localOnlySkills];
1771
+ return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
1358
1772
  }
1359
1773
  function isEmptySyncState(state) {
1360
1774
  return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
@@ -1381,14 +1795,20 @@ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
1381
1795
  skills: migratedSkills
1382
1796
  };
1383
1797
  }
1384
- async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}) {
1798
+ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}, acknowledgedNative = /* @__PURE__ */ new Map()) {
1385
1799
  const localSkillMap = toSkillMap(localSkills);
1386
1800
  const warnSkillSync = (message, error) => {
1387
1801
  console.warn(`[Notis] ${message}`, error);
1388
1802
  };
1389
1803
  let downloaded = 0;
1390
1804
  for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
1391
- if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
1805
+ const local = localSkillMap.get(cloudSkill.name);
1806
+ const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
1807
+ const legacyBaseline = Boolean(local && pendingLegacyUpgrade(cloudSkill, local, previousState) && previousNativeState(previousState, cloudSkill)?.folderHash === local.folderHash);
1808
+ if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
1809
+ if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
1810
+ failures.push({ name: cloudSkill.name, error: "Local native Skill changes were retained; resolve the version or identity conflict before using them." });
1811
+ }
1392
1812
  continue;
1393
1813
  }
1394
1814
  const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
@@ -1396,7 +1816,9 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
1396
1816
  writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
1397
1817
  // What the last write put there. Everything else in the folder was
1398
1818
  // produced locally and survives this one.
1399
- previouslyApplied: previousState.skills[skill.name]?.appliedFiles
1819
+ previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
1820
+ ...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {},
1821
+ ...legacyBaseline ? { legacyBaseline } : {}
1400
1822
  }),
1401
1823
  onWarning: warnSkillSync
1402
1824
  });
@@ -1413,6 +1835,8 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
1413
1835
  return downloaded;
1414
1836
  }
1415
1837
  function assertSkillsPullAuthorized(pullResponse) {
1838
+ if (!Array.isArray(pullResponse.skills)) throw new Error("Skill inventory is unavailable; existing files were retained.");
1839
+ assertNativePull(pullResponse);
1416
1840
  if (pullResponse.entitlement_access?.code === "entitlement_upgrade_required" && pullResponse.entitlement_access.entitlement === "skills") {
1417
1841
  throw new Error(
1418
1842
  "Skill sync access was denied; preserving existing local skills."
@@ -1430,11 +1854,13 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1430
1854
  "Cannot materialize skills without a valid authenticated desktop session."
1431
1855
  );
1432
1856
  }
1433
- const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId);
1434
- const pullResponse = await deps.pullSkills(serverUrl, jwt);
1857
+ let pullResponse = await deps.pullSkills(serverUrl, jwt);
1435
1858
  assertSkillsPullAuthorized(pullResponse);
1859
+ if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
1860
+ const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, void 0, pullResponse.sync_namespace);
1436
1861
  const previousState = await deps.readSyncState(syncPaths);
1437
1862
  const localSkills = await deps.scanLocalSkills(syncPaths);
1863
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
1438
1864
  const failedDownloads = [];
1439
1865
  const writtenSkillNames = /* @__PURE__ */ new Set();
1440
1866
  const appliedFilesByName = {};
@@ -1461,9 +1887,9 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1461
1887
  }
1462
1888
  if (relinkSkillNames.size > 0) {
1463
1889
  const relinked = await deps.syncSymlinks(
1464
- pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
1890
+ nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
1465
1891
  syncPaths.skillsDir,
1466
- { removeUndesired: false }
1892
+ { ...isolatedLinkOptions(syncPaths), removeUndesired: false }
1467
1893
  );
1468
1894
  failures.push(...(relinked.failures ?? []).filter(
1469
1895
  (failure) => !failedDownloads.some((download) => download.name === failure.name)
@@ -1480,7 +1906,8 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1480
1906
  verifiedLinks,
1481
1907
  new Set(failedDownloads.map((item) => item.name)),
1482
1908
  collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
1483
- collectAppliedFiles(pullResponse, previousState, appliedFilesByName)
1909
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
1910
+ previousState
1484
1911
  );
1485
1912
  for (const [name, entry] of Object.entries(materializedState.skills)) {
1486
1913
  if (writtenSkillNames.has(name)) continue;
@@ -1493,25 +1920,38 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1493
1920
  }
1494
1921
  }
1495
1922
  materializedState.skills = { ...previousState.skills, ...materializedState.skills };
1923
+ let removed = 0;
1924
+ for (const [name, previous] of Object.entries(previousState.skills)) {
1925
+ if (!previous.nativeReference && !previous.nativeUnavailable) continue;
1926
+ const current = pullResponse.skills.find((skill) => skill.id === previous.cloudId);
1927
+ const local = current && finalLocalSkills.find((row) => row.name === current.name);
1928
+ if (!current || !nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState)) {
1929
+ removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
1930
+ }
1931
+ }
1496
1932
  await deps.writeSyncState(materializedState, syncPaths);
1497
1933
  return {
1498
- materializedSkillNames: pullResponse.skills.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name) && !failedDownloads.some((failure) => failure.name === skill.name)).map((skill) => skill.name),
1934
+ materializedSkillNames: pullResponse.skills.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name) && (!isNativeSkill(skill) || skill.status === "active" && skill.agent_targets?.notis !== false && nativeLocalReady(skill, finalLocalSkills.find((local) => local.name === skill.name))) && !failedDownloads.some((failure) => failure.name === skill.name)).map((skill) => skill.name),
1499
1935
  pulled: pullResponse.skills.length,
1500
1936
  downloaded,
1501
1937
  deleted: 0,
1502
- removed: 0,
1938
+ removed,
1503
1939
  lastSyncedAt,
1504
1940
  ...failures.length ? { failedLinks: failures } : {}
1505
1941
  };
1506
1942
  }
1507
- async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures) {
1943
+ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures, linkOptions) {
1508
1944
  if (isEmptySyncState(scopedState)) {
1509
1945
  return 0;
1510
1946
  }
1511
1947
  const deletions = await deps.detectDeletedAgentSymlinks(
1512
- pullResponse.skills,
1948
+ pullResponse.skills.map((skill) => {
1949
+ const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : void 0;
1950
+ return previous ? { ...skill, name: previous[0] } : skill;
1951
+ }),
1513
1952
  previousState,
1514
- skillsDir
1953
+ skillsDir,
1954
+ linkOptions
1515
1955
  );
1516
1956
  if (deletions.length === 0) {
1517
1957
  return 0;
@@ -1527,21 +1967,34 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
1527
1967
  }
1528
1968
  const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1529
1969
  assertSkillsPullAuthorized(fresh);
1970
+ assertSyncEnvironment(fresh, pullResponse);
1530
1971
  Object.assign(pullResponse, fresh);
1531
1972
  let needsRefresh = false;
1532
1973
  let deactivated = 0;
1533
1974
  for (const [skillId, { skillName, agents }] of agentsBySkill) {
1534
1975
  const skill = pullResponse.skills.find((item) => item.id === skillId);
1535
1976
  const previous = previousState.skills[skillName];
1536
- if (!skill?.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
1977
+ if (!skill) continue;
1978
+ const native = isNativeSkill(skill);
1979
+ if (native) {
1980
+ if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
1981
+ } else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
1537
1982
  const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
1538
1983
  try {
1539
- const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch, skill.updated_at);
1540
- if (saved.success !== true || !saved.updated_at?.trim() || saved.updated_at === skill.updated_at || !["notis", "claude_code", "cursor", "codex"].every((agent) => typeof saved.agent_targets?.[agent] === "boolean") || ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
1984
+ const saved = await deps.updateAgentTargets(
1985
+ serverUrl,
1986
+ jwt,
1987
+ skillId,
1988
+ patch,
1989
+ native ? void 0 : skill.updated_at,
1990
+ native ? { reference: skill.native_reference, settingsRevision: skill.native_settings.revision } : void 0
1991
+ );
1992
+ if (saved.success !== true || !saved.updated_at?.trim() || !native && saved.updated_at === skill.updated_at || !["notis", "claude_code", "cursor", "codex"].every((agent) => typeof saved.agent_targets?.[agent] === "boolean") || ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
1541
1993
  throw new Error("Assignment update did not return a verified saved revision");
1542
1994
  }
1543
1995
  skill.agent_targets = saved.agent_targets;
1544
1996
  skill.updated_at = saved.updated_at;
1997
+ if (native) needsRefresh = true;
1545
1998
  deactivated += agents.size;
1546
1999
  } catch (error) {
1547
2000
  needsRefresh = true;
@@ -1552,14 +2005,17 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
1552
2005
  if (needsRefresh) {
1553
2006
  const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1554
2007
  assertSkillsPullAuthorized(refreshed);
2008
+ assertSyncEnvironment(refreshed, pullResponse);
1555
2009
  Object.assign(pullResponse, refreshed);
1556
2010
  }
1557
2011
  return deactivated;
1558
2012
  }
1559
2013
  async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
2014
+ const linkAgentFolders = options.linkAgentFolders !== false;
1560
2015
  const deps = {
1561
2016
  ...DEFAULT_RUN_SKILL_SYNC_DEPS,
1562
- ...dependencies
2017
+ ...dependencies,
2018
+ ...linkAgentFolders ? {} : MIRROR_ONLY_DEPS
1563
2019
  };
1564
2020
  const syncSettings = await deps.fetchSyncSettings(
1565
2021
  serverUrl,
@@ -1571,11 +2027,12 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1571
2027
  "Cannot sync skills without a server-verified account identity."
1572
2028
  );
1573
2029
  }
1574
- const syncPaths = getSkillSyncPathsForUser(syncUserId);
1575
- const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
1576
- syncPaths.skillsDir
1577
- );
2030
+ const syncPaths = getSkillSyncPathsForUser(syncUserId, void 0, syncSettings.sync_namespace, options.syncHome);
1578
2031
  if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
2032
+ const foreignLinksRemoved2 = await deps.removeForeignAccountSymlinks(
2033
+ syncPaths.skillsDir,
2034
+ isolatedLinkOptions(syncPaths)
2035
+ );
1579
2036
  return {
1580
2037
  syncEnabled: false,
1581
2038
  pushed: 0,
@@ -1584,7 +2041,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1584
2041
  deleted: 0,
1585
2042
  deactivated: 0,
1586
2043
  linked: 0,
1587
- removed: foreignLinksRemoved,
2044
+ removed: foreignLinksRemoved2,
1588
2045
  skipped: 0,
1589
2046
  lastSyncedAt: syncSettings.last_synced_at,
1590
2047
  failedPushes: []
@@ -1592,13 +2049,21 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1592
2049
  }
1593
2050
  let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1594
2051
  assertSkillsPullAuthorized(pullResponse);
2052
+ assertSyncEnvironment(pullResponse, syncSettings);
2053
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
2054
+ syncPaths.skillsDir,
2055
+ isolatedLinkOptions(syncPaths)
2056
+ );
2057
+ if (linkAgentFolders && !syncPaths.isolatedNamespace) {
2058
+ await forgetOtherAccountsAgentLinks(syncPaths);
2059
+ }
1595
2060
  const cloudCuratedSkillNames = new Set(
1596
2061
  pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
1597
2062
  );
1598
2063
  const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
1599
2064
  const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
1600
2065
  const assignmentFailures = [];
1601
- const deactivated = syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
2066
+ const deactivated = linkAgentFolders && !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
1602
2067
  serverUrl,
1603
2068
  jwt,
1604
2069
  pullResponse,
@@ -1606,41 +2071,55 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1606
2071
  scopedState,
1607
2072
  syncPaths.skillsDir,
1608
2073
  deps,
1609
- assignmentFailures
2074
+ assignmentFailures,
2075
+ isolatedLinkOptions(syncPaths)
1610
2076
  ) : 0;
2077
+ assertSyncEnvironment(pullResponse, syncSettings);
1611
2078
  const authUserId = decodeJwtSubject(jwt);
1612
2079
  let previousAuthState = null;
1613
- if (authUserId && authUserId !== syncUserId) {
1614
- const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
2080
+ if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
2081
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId, void 0, void 0, options.syncHome);
1615
2082
  previousAuthState = await deps.readSyncState(previousAuthPaths);
1616
2083
  await deps.gatherTopLevelLocalSkills(syncPaths, {
1617
2084
  sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
1618
2085
  protectedSkillNames
1619
2086
  });
1620
2087
  }
1621
- await deps.gatherTopLevelLocalSkills(syncPaths, {
1622
- protectedSkillNames
1623
- });
2088
+ if (linkAgentFolders && !syncPaths.isolatedNamespace) {
2089
+ await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
2090
+ }
1624
2091
  const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1625
2092
  const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
1626
2093
  localSkills,
1627
2094
  scopedState,
1628
- isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
2095
+ !syncPaths.isolatedNamespace && isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
1629
2096
  ));
2097
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
1630
2098
  const gatheredSymlinkResult = await deps.syncSymlinks(
1631
2099
  buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
1632
- syncPaths.skillsDir
2100
+ syncPaths.skillsDir,
2101
+ isolatedLinkOptions(syncPaths)
1633
2102
  );
1634
2103
  const pushCandidates = getPushCandidates(
1635
2104
  localSkills,
1636
2105
  previousState,
1637
2106
  cloudCuratedSkillNames,
1638
2107
  new Set(pullResponse.skills.map((skill) => skill.name)),
1639
- new Set(pullResponse.skills.filter((skill) => skill.app_owned || skill.owner_app_id).map((skill) => skill.name))
2108
+ pullResponse.skills
1640
2109
  );
1641
2110
  const failedPushes = [];
2111
+ const acknowledgedNative = /* @__PURE__ */ new Map();
2112
+ let writeLocalSkills = localSkills;
1642
2113
  if (pushCandidates.length > 0) {
1643
2114
  const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
2115
+ for (const result of pushResult.skills || []) {
2116
+ const saved = result;
2117
+ const candidate = pushCandidates.find((local) => local.nativeReference && local.cloudId === saved.id);
2118
+ if (candidate && saved.native_reference) {
2119
+ checkedNativeReference(saved.native_reference, candidate.cloudId);
2120
+ acknowledgedNative.set(candidate.cloudId, candidate.folderHash);
2121
+ }
2122
+ }
1644
2123
  if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
1645
2124
  failedPushes.push(...pushResult.failed);
1646
2125
  console.warn(
@@ -1649,13 +2128,21 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1649
2128
  }
1650
2129
  pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1651
2130
  assertSkillsPullAuthorized(pullResponse);
2131
+ assertSyncEnvironment(pullResponse, syncSettings);
2132
+ writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
2133
+ pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
1652
2134
  }
1653
2135
  const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
1654
2136
  let deleted = 0;
1655
2137
  for (const skillName of Object.keys(previousState.skills)) {
1656
2138
  if (!cloudSkillNames.has(skillName)) {
2139
+ const previous = previousState.skills[skillName];
2140
+ if (previous.nativeReference || previous.nativeUnavailable || pullResponse.skills.some((skill) => isNativeSkill(skill) && skill.id === previous.cloudId)) {
2141
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
2142
+ continue;
2143
+ }
1657
2144
  await deps.deleteLocalSkill(skillName, syncPaths);
1658
- await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
2145
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
1659
2146
  deleted += 1;
1660
2147
  }
1661
2148
  }
@@ -1664,18 +2151,20 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1664
2151
  const appliedFilesByName = {};
1665
2152
  const downloaded = await writePulledSkillsToScopedMirror(
1666
2153
  pullResponse,
1667
- localSkills,
2154
+ writeLocalSkills,
1668
2155
  previousState,
1669
2156
  syncPaths,
1670
2157
  deps,
1671
2158
  failedDownloads,
1672
2159
  writtenSkillNames,
1673
- appliedFilesByName
2160
+ appliedFilesByName,
2161
+ acknowledgedNative
1674
2162
  );
1675
2163
  const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1676
2164
  const symlinkResult = await deps.syncSymlinks(
1677
2165
  buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
1678
- syncPaths.skillsDir
2166
+ syncPaths.skillsDir,
2167
+ isolatedLinkOptions(syncPaths)
1679
2168
  );
1680
2169
  const verifiedLinks = { ...symlinkResult.verifiedAgentLinks ?? {} };
1681
2170
  for (const failure of failedDownloads) delete verifiedLinks[failure.name];
@@ -1688,7 +2177,8 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1688
2177
  verifiedLinks,
1689
2178
  new Set(failedDownloads.map((item) => item.name)),
1690
2179
  collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
1691
- collectAppliedFiles(pullResponse, previousState, appliedFilesByName)
2180
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
2181
+ previousState
1692
2182
  ),
1693
2183
  syncPaths
1694
2184
  );
@@ -1711,6 +2201,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1711
2201
  }
1712
2202
  export {
1713
2203
  fetchSyncSettings,
2204
+ isolatedLinkOptions,
1714
2205
  materializeCloudSkillsForLocalShell,
1715
2206
  runSkillSync,
1716
2207
  shouldWriteCloudSkill