@notis_ai/cli 0.2.0-beta.191.1 → 0.2.0-beta.196.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 (106) hide show
  1. package/README.md +479 -80
  2. package/config/portal-route-roots.json +48 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +29072 -2784
  4. package/dist/base-skills/notis-apps/SKILL.md +88 -67
  5. package/dist/base-skills/notis-apps/references/architecture.md +114 -167
  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 +104 -142
  10. package/dist/base-skills/notis-apps/references/sdk.md +113 -60
  11. package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
  12. package/dist/base-skills/notis-apps/references/views.md +142 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +4 -4
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
  15. package/dist/base-skills/notis-cli/references/native-databases.md +6 -6
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
  17. package/dist/base-skills/notis-query/SKILL.md +34 -60
  18. package/dist/base-skills/notis-query/references/database-discovery.md +59 -69
  19. package/dist/base-skills/notis-query/references/documents.md +63 -50
  20. package/dist/base-skills/notis-query/references/query.md +57 -524
  21. package/dist/sdk/src/bundleModule.ts +79 -0
  22. package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
  23. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  24. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  25. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  26. package/dist/sdk/src/config.ts +4 -26
  27. package/dist/sdk/src/documents.ts +2 -0
  28. package/dist/sdk/src/hooks/useAction.ts +28 -0
  29. package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
  30. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  31. package/dist/sdk/src/hooks/useHandover.ts +5 -4
  32. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  33. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  34. package/dist/sdk/src/hooks/useNotis.ts +3 -0
  35. package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
  36. package/dist/sdk/src/hooks/useQuery.ts +7 -4
  37. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  38. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  39. package/dist/sdk/src/index.ts +36 -0
  40. package/dist/sdk/src/interactions/shortcuts.tsx +17 -3
  41. package/dist/sdk/src/nativeData.ts +39 -0
  42. package/dist/sdk/src/presentation.tsx +82 -0
  43. package/dist/sdk/src/provider.tsx +9 -4
  44. package/dist/sdk/src/queryCache.ts +5 -3
  45. package/dist/sdk/src/queryErrors.ts +51 -0
  46. package/dist/sdk/src/runtime.ts +146 -8
  47. package/dist/sdk/src/space.ts +425 -0
  48. package/dist/sdk/src/styles.css +96 -72
  49. package/dist/sdk/src/vite.ts +11 -1
  50. package/dist/skill-sync/index.js +458 -68
  51. package/dist/skill-sync/index.js.map +4 -4
  52. package/dist/skill-sync-worker.mjs +451 -77
  53. package/dist/space-harness/host.js +66 -0
  54. package/dist/view-renderer/dist/host.css +1 -0
  55. package/dist/view-renderer/dist/host.js +1042 -0
  56. package/dist/view-renderer/dist/theme.css +73 -0
  57. package/dist/view-renderer/src/dom.js +180 -0
  58. package/dist/view-renderer/src/errors.js +8 -0
  59. package/dist/view-renderer/src/host.tsx +173 -0
  60. package/dist/view-renderer/src/index.js +4 -0
  61. package/dist/view-renderer/src/pool.js +76 -0
  62. package/dist/view-renderer/src/remote-broker.js +59 -0
  63. package/dist/view-renderer/src/renderer.js +201 -0
  64. package/package.json +11 -5
  65. package/skills/notis-apps/cli.md +270 -163
  66. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +2 -2
  67. package/skills/notis-onboarding/BRIEF.md +7 -3
  68. package/src/command-specs/apps.js +1 -8
  69. package/src/command-specs/files.js +29 -0
  70. package/src/command-specs/helpers.js +1 -0
  71. package/src/command-specs/index.js +10 -4
  72. package/src/command-specs/skills.js +229 -0
  73. package/src/command-specs/spaces.js +572 -0
  74. package/src/command-specs/views.js +69 -0
  75. package/src/runtime/agent-browser.js +30 -7
  76. package/src/runtime/app-boundary-validator.js +7 -4
  77. package/src/runtime/app-data-literals.js +82 -0
  78. package/src/runtime/app-platform.js +321 -253
  79. package/src/runtime/confined-space-build.js +74 -0
  80. package/src/runtime/confined-space-verify.js +49 -0
  81. package/src/runtime/native-record-files.js +47 -0
  82. package/src/runtime/retired-sdk-files.js +20 -0
  83. package/src/runtime/skill-sync/cloud-client.ts +26 -4
  84. package/src/runtime/skill-sync/index.ts +154 -35
  85. package/src/runtime/skill-sync/local-scanner.ts +125 -17
  86. package/src/runtime/skill-sync/native-identity.ts +102 -0
  87. package/src/runtime/skill-sync/native-plan.ts +59 -0
  88. package/src/runtime/skill-sync/sync-plan.ts +16 -9
  89. package/src/runtime/skill-sync/types.ts +32 -6
  90. package/src/runtime/source-root.js +32 -0
  91. package/src/runtime/space-collection-scaffold.js +66 -0
  92. package/src/runtime/space-database-structures.js +229 -0
  93. package/src/runtime/space-fixture-broker.js +50 -0
  94. package/src/runtime/space-lock.js +337 -0
  95. package/src/runtime/space-platform.js +105 -0
  96. package/src/runtime/space-source.js +281 -0
  97. package/src/runtime/space-test-server.js +327 -0
  98. package/src/runtime/space-verification.js +89 -0
  99. package/src/runtime/space-verify-browser-worker.js +51 -0
  100. package/src/runtime/space-view-manifest.js +254 -0
  101. package/src/runtime/sync-skills.js +1 -1
  102. package/src/runtime/transport.js +8 -1
  103. package/src/runtime/view-render.js +49 -0
  104. package/src/space-harness-host.ts +103 -0
  105. package/template/app/globals.css +1 -0
  106. package/src/command-specs/reports.js +0 -97
@@ -1,5 +1,6 @@
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 { join } from "node:path";
3
4
 
4
5
  // src/runtime/skill-sync/types.ts
5
6
  var DEFAULT_AGENT_TARGETS = {
@@ -17,8 +18,125 @@ function normalizeAgentTargets(targets) {
17
18
  };
18
19
  }
19
20
 
21
+ // src/runtime/skill-sync/native-identity.ts
22
+ import { createHash } from "node:crypto";
23
+ var NATIVE_IDENTITY_FILE = ".notis-native-skill.json";
24
+ var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
25
+ function checkedNativeReference(value, cloudId) {
26
+ const reference = value;
27
+ 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)) {
28
+ throw new Error("Native Skill identity is unavailable. Refresh before syncing.");
29
+ }
30
+ return structuredClone(reference);
31
+ }
32
+ function nativeDirectory(reference) {
33
+ return "native-" + checkedNativeReference(reference).skill_id;
34
+ }
35
+ function sameNativeReference(left, right) {
36
+ 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);
37
+ }
38
+ function isNativeSkill(skill) {
39
+ return skill.source === "native" || skill.native_reference !== void 0;
40
+ }
41
+ function assertNativePull(response) {
42
+ const ids = /* @__PURE__ */ new Set();
43
+ for (const skill of response.skills) {
44
+ if (!isNativeSkill(skill)) continue;
45
+ const reference = checkedNativeReference(skill.native_reference, skill.id);
46
+ 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") {
47
+ throw new Error("Native Skill inventory is incomplete. Existing local files were retained.");
48
+ }
49
+ const paths = /* @__PURE__ */ new Set();
50
+ let entrypoint = false;
51
+ for (const file of skill.bundle_files) {
52
+ 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) {
53
+ throw new Error("Native Skill bundle has a conflicting managed path.");
54
+ }
55
+ paths.add(file.path.toLowerCase());
56
+ if (file.path === "SKILL.md") {
57
+ entrypoint = Buffer.from(file.content_b64, "base64").equals(Buffer.from(skill.skill_md, "utf8"));
58
+ }
59
+ }
60
+ if (!entrypoint) throw new Error("Native Skill instructions and files do not match.");
61
+ ids.add(skill.id);
62
+ }
63
+ }
64
+ function nativeFileDigest(bytes) {
65
+ return createHash("sha256").update(bytes).digest("hex");
66
+ }
67
+ function nativeFrontmatterName(markdown) {
68
+ const header = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
69
+ return header?.[1].match(/^name\s*:\s*(.+)$/m)?.[1].trim().replace(/^['"]|['"]$/g, "") || "";
70
+ }
71
+ function checkedNativeIdentity(value) {
72
+ const identity = value;
73
+ 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))) {
74
+ throw new Error("Native Skill provenance is incomplete. Existing files were retained.");
75
+ }
76
+ return { ...identity, reference: checkedNativeReference(identity.reference, identity.cloudId) };
77
+ }
78
+ function nativeState(skill) {
79
+ if (!isNativeSkill(skill)) return {};
80
+ return {
81
+ nativeReference: checkedNativeReference(skill.native_reference, skill.id),
82
+ ...skill.native_settings ? { nativeSettingsRevision: skill.native_settings.revision } : {},
83
+ nativeDisplayName: skill.display_name || skill.name,
84
+ nativeScope: skill.native_scope,
85
+ nativeUnavailable: false
86
+ };
87
+ }
88
+
89
+ // src/runtime/skill-sync/native-plan.ts
90
+ function bindNativeFolders(response, locals, state) {
91
+ const nativeIds = new Set(response.skills.filter(isNativeSkill).map((skill) => skill.id));
92
+ for (const entry of Object.values(state.skills)) if (entry.nativeReference || entry.nativeUnavailable) nativeIds.add(entry.cloudId);
93
+ const byId = /* @__PURE__ */ new Map();
94
+ for (const local of locals) {
95
+ const id = local.cloudId || state.skills[local.name]?.cloudId;
96
+ if (!id || !nativeIds.has(id) && !local.nativeReference) continue;
97
+ if (local.nativeReference) checkedNativeReference(local.nativeReference, id);
98
+ if (byId.has(id)) throw new Error("Two local folders claim the same native Skill. Keep both copies and resolve the conflict.");
99
+ byId.set(id, local);
100
+ }
101
+ const skills = response.skills.map((skill) => isNativeSkill(skill) && byId.has(skill.id) ? { ...skill, name: byId.get(skill.id).name } : skill);
102
+ for (const skill of skills.filter(isNativeSkill)) {
103
+ if (skills.some((other) => other.id !== skill.id && other.name === skill.name)) {
104
+ throw new Error("A native Skill and another Skill share a local folder. Existing files were retained.");
105
+ }
106
+ }
107
+ return { ...response, skills };
108
+ }
109
+ function previousNativeState(state, skill) {
110
+ const named = state.skills[skill.name];
111
+ return named?.cloudId === skill.id ? named : Object.values(state.skills).find((entry) => entry.cloudId === skill.id);
112
+ }
113
+ function nativeLocalReady(skill, local) {
114
+ return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash && local.cloudId === skill.id && local.nativeScope === skill.native_scope && sameNativeReference(local.nativeReference, skill.native_reference));
115
+ }
116
+ function nativeLinkRows(response, locals) {
117
+ return response.skills.map((skill) => isNativeSkill(skill) && !nativeLocalReady(skill, locals.find((row) => row.name === skill.name)) ? { ...skill, status: "disabled" } : skill);
118
+ }
119
+ function retainMissingNativeState(previous, next) {
120
+ const present = new Set(Object.values(next.skills).map((row) => row.cloudId));
121
+ 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: {} }]));
122
+ const history = structuredClone(previous.nativeHistory || {});
123
+ for (const [name, row] of [...Object.entries(previous.skills), ...Object.entries(next.skills)]) {
124
+ if (!row.nativeReference && !row.nativeUnavailable) continue;
125
+ const names = history[row.cloudId]?.names || [];
126
+ history[row.cloudId] = { names: [.../* @__PURE__ */ new Set([...names, name])] };
127
+ }
128
+ return {
129
+ ...next,
130
+ skills: { ...retained, ...next.skills },
131
+ ...Object.keys(history).length ? { nativeHistory: history } : {}
132
+ };
133
+ }
134
+ function hasNativeFolderHistory(state, name) {
135
+ return Object.values(state.nativeHistory || {}).some((row) => row.names.includes(name));
136
+ }
137
+
20
138
  // src/runtime/skill-sync/local-scanner.ts
21
- import { createHash } from "crypto";
139
+ import { createHash as createHash2 } from "crypto";
22
140
  import { execFile } from "child_process";
23
141
  import { promises as fs } from "fs";
24
142
  import os from "os";
@@ -26,6 +144,8 @@ import path from "path";
26
144
  import { promisify } from "util";
27
145
  var HOME_DIR = os.homedir();
28
146
  var execFileAsync = promisify(execFile);
147
+ var NativeSkillIdentityError = class extends Error {
148
+ };
29
149
  var AGENTS_DIR = path.join(HOME_DIR, ".agents");
30
150
  var LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
31
151
  var NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
@@ -85,15 +205,18 @@ function sanitizePathSegment(value) {
85
205
  if (sanitized === raw) {
86
206
  return sanitized;
87
207
  }
88
- return `${sanitized}-${createHash("sha256").update(raw).digest("hex").slice(0, 12)}`;
208
+ return `${sanitized}-${createHash2("sha256").update(raw).digest("hex").slice(0, 12)}`;
89
209
  }
90
- function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR) {
210
+ function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR, namespace, syncHome) {
91
211
  const safeUserId = sanitizePathSegment(authUserId);
92
- const resolvedAgentsDir = path.resolve(agentsDir);
93
- const syncRoot = getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
94
- const userRoot = path.join(syncRoot, "users", safeUserId);
212
+ const homeDir = syncHome ? path.resolve(syncHome) : void 0;
213
+ const resolvedAgentsDir = homeDir ? path.join(homeDir, ".agents") : path.resolve(agentsDir);
214
+ const syncRoot = homeDir ? path.join(homeDir, ".notis", "skills") : getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
215
+ const userRoot = namespace ? path.join(syncRoot, "environments", sanitizePathSegment(namespace), "users", safeUserId) : path.join(syncRoot, "users", safeUserId);
95
216
  const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
96
217
  return {
218
+ ...homeDir ? { homeDir } : {},
219
+ ...namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {},
97
220
  agentsDir: resolvedAgentsDir,
98
221
  syncRoot,
99
222
  legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
@@ -148,16 +271,17 @@ function isIgnoredSkillEntry(entry) {
148
271
  if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
149
272
  return /\.py[co]$/i.test(entry.name);
150
273
  }
151
- async function listFilesRecursive(dirPath) {
274
+ async function listFilesRecursive(dirPath, rootPath = dirPath, strictLinks = false) {
152
275
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
153
276
  const nested = await Promise.all(
154
277
  entries.map(async (entry) => {
155
- if (isIgnoredSkillEntry(entry)) {
278
+ if (isIgnoredSkillEntry(entry) || dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE) {
156
279
  return [];
157
280
  }
158
281
  const fullPath = path.join(dirPath, entry.name);
282
+ if (strictLinks && entry.isSymbolicLink()) throw new Error("Native Skill bundles cannot contain symbolic links.");
159
283
  if (entry.isDirectory()) {
160
- return listFilesRecursive(fullPath);
284
+ return listFilesRecursive(fullPath, rootPath, strictLinks);
161
285
  }
162
286
  if (entry.isFile()) {
163
287
  return [fullPath];
@@ -168,7 +292,7 @@ async function listFilesRecursive(dirPath) {
168
292
  return nested.flat().sort();
169
293
  }
170
294
  async function computeFolderHash(dirPath) {
171
- const hash = createHash("sha256");
295
+ const hash = createHash2("sha256");
172
296
  const filePaths = await listFilesRecursive(dirPath);
173
297
  for (const filePath of filePaths) {
174
298
  const relativePath = path.relative(dirPath, filePath);
@@ -246,17 +370,17 @@ function defaultTopLevelSkillSources(paths) {
246
370
  { label: "agents", root: paths.legacySkillsDir, priority: 2 },
247
371
  {
248
372
  label: "codex",
249
- root: path.join(HOME_DIR, ".codex", "skills"),
373
+ root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
250
374
  priority: 3
251
375
  },
252
376
  {
253
377
  label: "cursor",
254
- root: path.join(HOME_DIR, ".cursor", "skills"),
378
+ root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
255
379
  priority: 4
256
380
  },
257
381
  {
258
382
  label: "claude",
259
- root: path.join(HOME_DIR, ".claude", "skills"),
383
+ root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
260
384
  priority: 5
261
385
  }
262
386
  );
@@ -476,6 +600,7 @@ async function ensureCanonicalSkillsDir(paths = DEFAULT_SYNC_PATHS) {
476
600
  async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
477
601
  await ensureCanonicalSkillsDir(paths);
478
602
  const sourceUrls = await readSkillLock(paths);
603
+ const previousState = await readSyncState(paths);
479
604
  const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
480
605
  const skills = await Promise.all(
481
606
  entries.map(async (entry) => {
@@ -484,6 +609,15 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
484
609
  }
485
610
  const skillDir = path.join(paths.skillsDir, entry.name);
486
611
  const skillMdPath = path.join(skillDir, "SKILL.md");
612
+ let marker = null;
613
+ try {
614
+ marker = await fs.readFile(path.join(skillDir, NATIVE_IDENTITY_FILE), "utf8");
615
+ } catch (error) {
616
+ if (error.code !== "ENOENT") {
617
+ throw new NativeSkillIdentityError("Native Skill provenance could not be read; existing files were retained.");
618
+ }
619
+ }
620
+ const knownNative = marker !== null || Boolean(previousState.skills[entry.name]?.nativeReference) || Object.values(previousState.nativeHistory || {}).some((row) => row.names.includes(entry.name));
487
621
  try {
488
622
  const skillMd = await fs.readFile(skillMdPath, "utf8");
489
623
  const { description } = parseFrontMatter(skillMd);
@@ -495,11 +629,38 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
495
629
  folderHash,
496
630
  directoryPath: skillDir
497
631
  };
632
+ if (marker !== null) {
633
+ let identity;
634
+ try {
635
+ identity = checkedNativeIdentity(JSON.parse(marker));
636
+ } catch {
637
+ throw new NativeSkillIdentityError("Native Skill provenance is unreadable; do not upload it as a new Skill.");
638
+ }
639
+ let current = true;
640
+ for (const [name, expected] of Object.entries(identity.files)) {
641
+ try {
642
+ if (nativeFileDigest(await fs.readFile(resolveSkillBundleFilePath(skillDir, name))) !== expected) current = false;
643
+ } catch {
644
+ current = false;
645
+ }
646
+ }
647
+ const editedName = nativeFrontmatterName(skillMd);
648
+ Object.assign(skill, {
649
+ cloudId: identity.cloudId,
650
+ nativeReference: identity.reference,
651
+ nativeScope: identity.scope,
652
+ nativeBaseFolderHash: identity.folderHash,
653
+ nativeFilesCurrent: current,
654
+ displayName: editedName && editedName !== identity.frontmatterName ? editedName : identity.displayName
655
+ });
656
+ }
498
657
  if (sourceUrls[entry.name]) {
499
658
  skill.sourceUrl = sourceUrls[entry.name];
500
659
  }
501
660
  return skill;
502
- } catch {
661
+ } catch (error) {
662
+ if (error instanceof NativeSkillIdentityError) throw error;
663
+ if (knownNative) throw new NativeSkillIdentityError("Native Skill files could not be read; existing files were retained.");
503
664
  return null;
504
665
  }
505
666
  })
@@ -510,6 +671,9 @@ function normalizeSyncState(state) {
510
671
  if (!state || state.version !== 1 || typeof state.skills !== "object") {
511
672
  return null;
512
673
  }
674
+ 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")))) {
675
+ throw new Error("Native Skill sync provenance is unreadable; existing files were retained.");
676
+ }
513
677
  const normalizeStoredAgentTargets = (targets) => ({
514
678
  notis: Boolean(targets?.notis ?? true),
515
679
  claude_code: Boolean(targets?.claude_code ?? true),
@@ -518,6 +682,7 @@ function normalizeSyncState(state) {
518
682
  });
519
683
  return {
520
684
  version: 1,
685
+ ...state.nativeHistory ? { nativeHistory: state.nativeHistory } : {},
521
686
  lastSyncedAt: typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
522
687
  skills: Object.fromEntries(
523
688
  Object.entries(state.skills || {}).map(([skillName, skillState]) => [
@@ -677,13 +842,18 @@ async function listRelativeFiles(dirPath) {
677
842
  function toPosixRelativePath(rootDir, filePath) {
678
843
  return path.relative(rootDir, filePath).split(path.sep).join("/");
679
844
  }
680
- async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied) {
845
+ async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied, strictConflicts = false) {
681
846
  const managed = previouslyApplied ? new Set(previouslyApplied) : null;
682
847
  for (const existingFile of await listFilesRecursive(existingDir)) {
683
848
  const relativePath = toPosixRelativePath(existingDir, existingFile);
684
849
  if (managed?.has(relativePath)) continue;
685
850
  const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
686
- if (await pathExists(stagedPath)) continue;
851
+ if (await pathExists(stagedPath)) {
852
+ if (strictConflicts && !(await fs.readFile(existingFile)).equals(await fs.readFile(stagedPath))) {
853
+ throw new Error("An incoming native Skill file conflicts with a local file. Local files were retained.");
854
+ }
855
+ continue;
856
+ }
687
857
  await fs.mkdir(path.dirname(stagedPath), { recursive: true });
688
858
  await fs.copyFile(existingFile, stagedPath);
689
859
  }
@@ -711,13 +881,41 @@ async function replaceSkillDirectoryAtomically(skillDir, populateDir, preserve)
711
881
  let promotedStaging = false;
712
882
  let cleanupError = null;
713
883
  try {
884
+ if (preserve?.native && preserve.expectedFolderHash === void 0) {
885
+ let existing = false;
886
+ try {
887
+ await fs.lstat(skillDir);
888
+ existing = true;
889
+ } catch (error) {
890
+ if (error.code !== "ENOENT") throw error;
891
+ }
892
+ if (existing) throw new Error("Existing native Skill files need a readable baseline before replacement.");
893
+ }
894
+ if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
895
+ throw new Error("Local Skill files changed before sync; the newer local edits were retained.");
896
+ }
714
897
  await populateDir(stagingDir);
715
898
  if (preserve && await pathExists(skillDir)) {
716
- await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied);
899
+ await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied, preserve.native);
900
+ }
901
+ if (preserve?.native) {
902
+ const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
903
+ const marker = checkedNativeIdentity(JSON.parse(await fs.readFile(markerPath, "utf8")));
904
+ marker.folderHash = await computeFolderHash(stagingDir);
905
+ await fs.writeFile(markerPath, JSON.stringify(marker), { mode: 384 });
906
+ }
907
+ if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
908
+ throw new Error("Local Skill files changed during sync; the newer local edits were retained.");
717
909
  }
718
910
  if (await pathExists(skillDir)) {
719
911
  await fs.rename(skillDir, backupDir);
720
912
  movedExisting = true;
913
+ if (preserve?.native && preserve.expectedFolderHash === void 0) {
914
+ throw new Error("Native Skill files appeared during sync; existing files were retained.");
915
+ }
916
+ if (preserve?.expectedFolderHash !== void 0 && await computeFolderHash(backupDir) !== preserve.expectedFolderHash) {
917
+ throw new Error("Local Skill files changed before publication; the newer local edits were retained.");
918
+ }
721
919
  }
722
920
  await fs.rename(stagingDir, skillDir);
723
921
  promotedStaging = true;
@@ -762,12 +960,41 @@ async function createSkillBundleBase64(skill) {
762
960
  await fs.rm(zipPath, { force: true });
763
961
  }
764
962
  }
963
+ async function captureNativeSkillFiles(skill) {
964
+ const hash = createHash2("sha256");
965
+ const files = [];
966
+ for (const filePath of await listFilesRecursive(skill.directoryPath, skill.directoryPath, true)) {
967
+ const relative = path.relative(skill.directoryPath, filePath);
968
+ const bytes = await fs.readFile(filePath);
969
+ hash.update(relative);
970
+ hash.update("\0");
971
+ hash.update(bytes);
972
+ hash.update("\0");
973
+ files.push({ path: relative.split(path.sep).join("/"), content_b64: bytes.toString("base64") });
974
+ }
975
+ if (hash.digest("hex") !== skill.folderHash) throw new Error("Local Skill files changed before upload. Sync again with the current files.");
976
+ return files;
977
+ }
765
978
  async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PATHS, options = {}) {
979
+ if (isNativeSkill(skill)) {
980
+ assertNativePull({
981
+ skills: [{ ...skill, name: "native-" + skill.id }],
982
+ skill_sync_protocol: 2,
983
+ sync_scope: skill.native_scope,
984
+ sync_enabled: true,
985
+ last_synced_at: null
986
+ });
987
+ if (bundleBytes?.length) throw new Error("Native Skills require their verified complete file response.");
988
+ }
766
989
  const skillDir = path.join(
767
990
  paths.skillsDir,
768
991
  safeName(skill.name, paths.skillsDir)
769
992
  );
770
- const preserve = { previouslyApplied: options.previouslyApplied };
993
+ const preserve = {
994
+ previouslyApplied: options.previouslyApplied,
995
+ expectedFolderHash: options.expectedFolderHash,
996
+ native: isNativeSkill(skill)
997
+ };
771
998
  if (bundleBytes?.length) {
772
999
  const bundlePath = path.join(
773
1000
  os.tmpdir(),
@@ -807,6 +1034,20 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
807
1034
  );
808
1035
  appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
809
1036
  }
1037
+ if (skill.native_reference) {
1038
+ const identity = {
1039
+ schema: 1,
1040
+ cloudId: skill.id,
1041
+ reference: skill.native_reference,
1042
+ displayName: skill.display_name || skill.name,
1043
+ frontmatterName: nativeFrontmatterName(skill.skill_md || ""),
1044
+ scope: skill.native_scope,
1045
+ folderHash: await computeFolderHash(stagingDir),
1046
+ files: Object.fromEntries((skill.bundle_files || []).map((file) => [file.path, nativeFileDigest(Buffer.from(file.content_b64, "base64"))]))
1047
+ };
1048
+ await fs.writeFile(path.join(stagingDir, NATIVE_IDENTITY_FILE), JSON.stringify(identity), { mode: 384 });
1049
+ appliedFiles.push(NATIVE_IDENTITY_FILE);
1050
+ }
810
1051
  }, preserve);
811
1052
  return { written: true, appliedFiles: appliedFiles.sort() };
812
1053
  }
@@ -821,6 +1062,7 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
821
1062
  }
822
1063
 
823
1064
  // src/runtime/skill-sync/cloud-client.ts
1065
+ import { createHash as createHash3 } from "node:crypto";
824
1066
  async function requestJson(url, jwt, options = {}) {
825
1067
  const response = await fetch(url, {
826
1068
  signal: AbortSignal.timeout(9e4),
@@ -844,7 +1086,7 @@ async function fetchSyncSettings(serverUrl, jwt) {
844
1086
  }
845
1087
  async function pullSkills(serverUrl, jwt) {
846
1088
  return requestJson(`${serverUrl}/portal_skills/sync-pull`, jwt, {
847
- body: {}
1089
+ body: { skill_sync_protocol: 2 }
848
1090
  });
849
1091
  }
850
1092
  async function pushChangedSkills(serverUrl, jwt, changedSkills) {
@@ -854,10 +1096,17 @@ async function pushChangedSkills(serverUrl, jwt, changedSkills) {
854
1096
  skill_md: skill.skillMd,
855
1097
  source_url: skill.sourceUrl,
856
1098
  folder_hash: skill.folderHash,
857
- bundle_base64: await createSkillBundleBase64(skill)
1099
+ ...skill.nativeReference ? { bundle_files: await captureNativeSkillFiles(skill) } : { bundle_base64: await createSkillBundleBase64(skill) },
1100
+ ...skill.cloudId ? { cloud_id: skill.cloudId } : {},
1101
+ ...skill.nativeReference ? {
1102
+ native_reference: skill.nativeReference,
1103
+ display_name: skill.displayName,
1104
+ native_scope: skill.nativeScope
1105
+ } : {}
858
1106
  })));
859
1107
  return requestJson(`${serverUrl}/portal_skills/sync-push`, jwt, {
860
1108
  body: {
1109
+ skill_sync_protocol: 2,
861
1110
  skills: payloadSkills
862
1111
  }
863
1112
  });
@@ -870,7 +1119,26 @@ async function downloadSkillBundle(bundleUrl) {
870
1119
  }
871
1120
  return Buffer.from(await response.arrayBuffer());
872
1121
  }
873
- async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt) {
1122
+ async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt, native) {
1123
+ if (native) {
1124
+ checkedNativeReference(native.reference, skillId);
1125
+ if (!Number.isSafeInteger(native.settingsRevision) || native.settingsRevision < 0) throw new Error("Refresh native Skill settings before changing assignments.");
1126
+ const payload = {
1127
+ operation: "update",
1128
+ target: { skill_id: skillId, access_revision: native.reference.access_revision },
1129
+ revision: native.settingsRevision,
1130
+ patch: { agent_targets: Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b))) }
1131
+ };
1132
+ const requestId = "sync-settings-" + createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
1133
+ const saved = await requestJson(
1134
+ `${serverUrl}/portal_skills/native-settings`,
1135
+ jwt,
1136
+ { body: { ...payload, request_id: requestId } }
1137
+ );
1138
+ const current = saved.current || saved;
1139
+ 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.");
1140
+ return { success: true, agent_targets: current.settings.agent_targets, updated_at: current.settings.updated_at };
1141
+ }
874
1142
  return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
875
1143
  method: "PATCH",
876
1144
  body: {
@@ -1196,22 +1464,27 @@ function selectCloudSkillsToApply(skills) {
1196
1464
  }
1197
1465
  return [...winners.values()];
1198
1466
  }
1199
- function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudAppOwnedSkillNames = /* @__PURE__ */ new Set()) {
1467
+ function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudSkills = []) {
1200
1468
  return localSkills.filter((skill) => {
1201
1469
  if (cloudCuratedSkillNames.has(skill.name)) {
1202
1470
  return false;
1203
1471
  }
1204
- if (cloudAppOwnedSkillNames.has(skill.name)) {
1205
- return false;
1472
+ const previous = syncState.skills[skill.name] || (skill.cloudId ? Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId) : void 0);
1473
+ const native = cloudSkills.find((row) => isNativeSkill(row) && row.id === (skill.cloudId || previous?.cloudId));
1474
+ if (native || skill.nativeReference || previous?.nativeReference || previous?.nativeUnavailable) {
1475
+ return Boolean(native && skill.nativeReference && skill.nativeScope === native.native_scope && (previous ? previous.folderHash !== skill.folderHash : skill.nativeBaseFolderHash !== skill.folderHash));
1206
1476
  }
1207
- const previous = syncState.skills[skill.name];
1208
1477
  if (!previous) {
1478
+ if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
1209
1479
  return true;
1210
1480
  }
1211
1481
  if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
1212
1482
  return false;
1213
1483
  }
1214
1484
  return previous.folderHash !== skill.folderHash;
1485
+ }).map((skill) => {
1486
+ const previous = syncState.skills[skill.name] || Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId);
1487
+ return { ...skill, ...skill.cloudId || previous?.cloudId ? { cloudId: skill.cloudId || previous.cloudId } : {} };
1215
1488
  });
1216
1489
  }
1217
1490
 
@@ -1287,6 +1560,24 @@ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
1287
1560
  removeForeignAccountSymlinks,
1288
1561
  updateAgentTargets
1289
1562
  };
1563
+ function isolatedLinkOptions(paths) {
1564
+ if (!paths.isolatedNamespace && !paths.homeDir) return void 0;
1565
+ const root = paths.isolatedNamespace ? join(paths.syncRoot, "environments", paths.isolatedNamespace, "agents") : paths.homeDir;
1566
+ return {
1567
+ agentSkillDirs: {
1568
+ notis: paths.skillsDir,
1569
+ claude_code: join(root, ".claude", "skills"),
1570
+ cursor: join(root, ".cursor", "skills"),
1571
+ codex: join(root, ".codex", "skills")
1572
+ },
1573
+ legacyGlobalSkillsDir: join(root, ".agents", "skills")
1574
+ };
1575
+ }
1576
+ function assertSyncEnvironment(pull, settings) {
1577
+ if ((pull.sync_namespace || null) !== (settings.sync_namespace || null) || settings.sync_scope !== void 0 && pull.sync_scope !== settings.sync_scope) {
1578
+ throw new Error("Skill sync environment changed. Existing mirrors were retained.");
1579
+ }
1580
+ }
1290
1581
  function toSkillMap(skills) {
1291
1582
  return new Map(skills.map((skill) => [skill.name, skill]));
1292
1583
  }
@@ -1303,7 +1594,7 @@ function decodeJwtSubject(jwt) {
1303
1594
  }
1304
1595
  }
1305
1596
  function cloudContentHash(skill) {
1306
- return createHash2("sha256").update(JSON.stringify({
1597
+ return createHash4("sha256").update(JSON.stringify({
1307
1598
  md: skill.skill_md,
1308
1599
  hash: skill.skill_folder_hash,
1309
1600
  source: skill.skill_source_url,
@@ -1318,6 +1609,12 @@ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
1318
1609
  if (!localSkill) {
1319
1610
  return true;
1320
1611
  }
1612
+ if (isNativeSkill(cloudSkill)) {
1613
+ if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
1614
+ if (nativeLocalReady(cloudSkill, localSkill)) return false;
1615
+ const nativePrevious = previousNativeState(previousState, cloudSkill);
1616
+ return localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash || !localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id && nativePrevious.folderHash === localSkill.folderHash;
1617
+ }
1321
1618
  const previous = previousState.skills[skillName];
1322
1619
  if (previous?.folderHash === localSkill.folderHash && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
1323
1620
  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) {
@@ -1343,17 +1640,28 @@ function collectAppliedFiles(pullResponse, previousState, appliedFilesByName) {
1343
1640
  }
1344
1641
  return applied;
1345
1642
  }
1346
- function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}) {
1643
+ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}, previousState = { version: 1, lastSyncedAt: null, skills: {} }) {
1347
1644
  const localSkillMap = toSkillMap(localSkills);
1348
1645
  const skills = Object.fromEntries(
1349
1646
  selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
1350
1647
  const localSkill = localSkillMap.get(skill.name);
1351
1648
  const appliedCloudFolderHash = appliedRevisions[skill.name];
1352
1649
  const appliedFileList = appliedFiles[skill.name];
1650
+ if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
1651
+ const previous = previousNativeState(previousState, skill);
1652
+ return [skill.name, { ...previous || {
1653
+ cloudId: skill.id,
1654
+ folderHash: localSkill?.nativeBaseFolderHash || "",
1655
+ agentTargets: normalizeAgentTargets(skill.agent_targets),
1656
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString(),
1657
+ ...localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}
1658
+ }, nativeUnavailable: true, verifiedAgentLinks: {} }];
1659
+ }
1353
1660
  return [
1354
1661
  skill.name,
1355
1662
  {
1356
1663
  cloudId: skill.id,
1664
+ ...nativeState(skill),
1357
1665
  folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
1358
1666
  agentTargets: normalizeAgentTargets(skill.agent_targets),
1359
1667
  verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
@@ -1366,17 +1674,30 @@ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLi
1366
1674
  ];
1367
1675
  })
1368
1676
  );
1369
- return {
1677
+ for (const local of localSkills) {
1678
+ if (!local.nativeReference || Object.values(skills).some((row) => row.cloudId === local.cloudId)) continue;
1679
+ skills[local.name] = {
1680
+ cloudId: local.cloudId,
1681
+ nativeReference: local.nativeReference,
1682
+ nativeScope: local.nativeScope,
1683
+ nativeUnavailable: true,
1684
+ verifiedAgentLinks: {},
1685
+ folderHash: local.nativeBaseFolderHash || "",
1686
+ agentTargets: normalizeAgentTargets(),
1687
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
1688
+ };
1689
+ }
1690
+ return retainMissingNativeState(previousState, {
1370
1691
  version: 1,
1371
1692
  lastSyncedAt,
1372
1693
  skills
1373
- };
1694
+ });
1374
1695
  }
1375
1696
  function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1376
1697
  const cloudSkillNames = new Set(
1377
1698
  pullResponse.skills.map((skill) => skill.name)
1378
1699
  );
1379
- const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).map((skill) => {
1700
+ 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) => {
1380
1701
  const previous = previousState.skills[skill.name];
1381
1702
  return {
1382
1703
  id: previous?.cloudId || `local-${skill.name}`,
@@ -1389,7 +1710,7 @@ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1389
1710
  status: "active"
1390
1711
  };
1391
1712
  });
1392
- return [...pullResponse.skills, ...localOnlySkills];
1713
+ return [...nativeLinkRows(pullResponse, localSkills), ...localOnlySkills];
1393
1714
  }
1394
1715
  function isEmptySyncState(state) {
1395
1716
  return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
@@ -1416,14 +1737,19 @@ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
1416
1737
  skills: migratedSkills
1417
1738
  };
1418
1739
  }
1419
- async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}) {
1740
+ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}, acknowledgedNative = /* @__PURE__ */ new Map()) {
1420
1741
  const localSkillMap = toSkillMap(localSkills);
1421
1742
  const warnSkillSync = (message, error) => {
1422
1743
  console.warn(`[Notis] ${message}`, error);
1423
1744
  };
1424
1745
  let downloaded = 0;
1425
1746
  for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
1426
- if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
1747
+ const local = localSkillMap.get(cloudSkill.name);
1748
+ const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
1749
+ if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
1750
+ if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
1751
+ failures.push({ name: cloudSkill.name, error: "Local native Skill changes were retained; resolve the version or identity conflict before using them." });
1752
+ }
1427
1753
  continue;
1428
1754
  }
1429
1755
  const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
@@ -1431,7 +1757,8 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
1431
1757
  writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
1432
1758
  // What the last write put there. Everything else in the folder was
1433
1759
  // produced locally and survives this one.
1434
- previouslyApplied: previousState.skills[skill.name]?.appliedFiles
1760
+ previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
1761
+ ...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}
1435
1762
  }),
1436
1763
  onWarning: warnSkillSync
1437
1764
  });
@@ -1448,6 +1775,8 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
1448
1775
  return downloaded;
1449
1776
  }
1450
1777
  function assertSkillsPullAuthorized(pullResponse) {
1778
+ if (!Array.isArray(pullResponse.skills)) throw new Error("Skill inventory is unavailable; existing files were retained.");
1779
+ assertNativePull(pullResponse);
1451
1780
  if (pullResponse.entitlement_access?.code === "entitlement_upgrade_required" && pullResponse.entitlement_access.entitlement === "skills") {
1452
1781
  throw new Error(
1453
1782
  "Skill sync access was denied; preserving existing local skills."
@@ -1465,11 +1794,13 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1465
1794
  "Cannot materialize skills without a valid authenticated desktop session."
1466
1795
  );
1467
1796
  }
1468
- const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId);
1469
- const pullResponse = await deps.pullSkills(serverUrl, jwt);
1797
+ let pullResponse = await deps.pullSkills(serverUrl, jwt);
1470
1798
  assertSkillsPullAuthorized(pullResponse);
1799
+ if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
1800
+ const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, void 0, pullResponse.sync_namespace);
1471
1801
  const previousState = await deps.readSyncState(syncPaths);
1472
1802
  const localSkills = await deps.scanLocalSkills(syncPaths);
1803
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
1473
1804
  const failedDownloads = [];
1474
1805
  const writtenSkillNames = /* @__PURE__ */ new Set();
1475
1806
  const appliedFilesByName = {};
@@ -1496,9 +1827,9 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1496
1827
  }
1497
1828
  if (relinkSkillNames.size > 0) {
1498
1829
  const relinked = await deps.syncSymlinks(
1499
- pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
1830
+ nativeLinkRows(pullResponse, finalLocalSkills).filter((skill) => relinkSkillNames.has(skill.name)),
1500
1831
  syncPaths.skillsDir,
1501
- { removeUndesired: false }
1832
+ { ...isolatedLinkOptions(syncPaths), removeUndesired: false }
1502
1833
  );
1503
1834
  failures.push(...(relinked.failures ?? []).filter(
1504
1835
  (failure) => !failedDownloads.some((download) => download.name === failure.name)
@@ -1515,7 +1846,8 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1515
1846
  verifiedLinks,
1516
1847
  new Set(failedDownloads.map((item) => item.name)),
1517
1848
  collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
1518
- collectAppliedFiles(pullResponse, previousState, appliedFilesByName)
1849
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
1850
+ previousState
1519
1851
  );
1520
1852
  for (const [name, entry] of Object.entries(materializedState.skills)) {
1521
1853
  if (writtenSkillNames.has(name)) continue;
@@ -1528,25 +1860,37 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
1528
1860
  }
1529
1861
  }
1530
1862
  materializedState.skills = { ...previousState.skills, ...materializedState.skills };
1863
+ let removed = 0;
1864
+ for (const [name, previous] of Object.entries(previousState.skills)) {
1865
+ if (!previous.nativeReference && !previous.nativeUnavailable) continue;
1866
+ const current = pullResponse.skills.find((skill) => skill.id === previous.cloudId);
1867
+ if (!current || !nativeLocalReady(current, finalLocalSkills.find((local) => local.name === current.name))) {
1868
+ removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
1869
+ }
1870
+ }
1531
1871
  await deps.writeSyncState(materializedState, syncPaths);
1532
1872
  return {
1533
- materializedSkillNames: pullResponse.skills.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name) && !failedDownloads.some((failure) => failure.name === skill.name)).map((skill) => skill.name),
1873
+ 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),
1534
1874
  pulled: pullResponse.skills.length,
1535
1875
  downloaded,
1536
1876
  deleted: 0,
1537
- removed: 0,
1877
+ removed,
1538
1878
  lastSyncedAt,
1539
1879
  ...failures.length ? { failedLinks: failures } : {}
1540
1880
  };
1541
1881
  }
1542
- async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures) {
1882
+ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures, linkOptions) {
1543
1883
  if (isEmptySyncState(scopedState)) {
1544
1884
  return 0;
1545
1885
  }
1546
1886
  const deletions = await deps.detectDeletedAgentSymlinks(
1547
- pullResponse.skills,
1887
+ pullResponse.skills.map((skill) => {
1888
+ const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : void 0;
1889
+ return previous ? { ...skill, name: previous[0] } : skill;
1890
+ }),
1548
1891
  previousState,
1549
- skillsDir
1892
+ skillsDir,
1893
+ linkOptions
1550
1894
  );
1551
1895
  if (deletions.length === 0) {
1552
1896
  return 0;
@@ -1562,21 +1906,34 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
1562
1906
  }
1563
1907
  const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1564
1908
  assertSkillsPullAuthorized(fresh);
1909
+ assertSyncEnvironment(fresh, pullResponse);
1565
1910
  Object.assign(pullResponse, fresh);
1566
1911
  let needsRefresh = false;
1567
1912
  let deactivated = 0;
1568
1913
  for (const [skillId, { skillName, agents }] of agentsBySkill) {
1569
1914
  const skill = pullResponse.skills.find((item) => item.id === skillId);
1570
1915
  const previous = previousState.skills[skillName];
1571
- if (!skill?.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
1916
+ if (!skill) continue;
1917
+ const native = isNativeSkill(skill);
1918
+ if (native) {
1919
+ if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
1920
+ } else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
1572
1921
  const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
1573
1922
  try {
1574
- const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch, skill.updated_at);
1575
- 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)) {
1923
+ const saved = await deps.updateAgentTargets(
1924
+ serverUrl,
1925
+ jwt,
1926
+ skillId,
1927
+ patch,
1928
+ native ? void 0 : skill.updated_at,
1929
+ native ? { reference: skill.native_reference, settingsRevision: skill.native_settings.revision } : void 0
1930
+ );
1931
+ 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)) {
1576
1932
  throw new Error("Assignment update did not return a verified saved revision");
1577
1933
  }
1578
1934
  skill.agent_targets = saved.agent_targets;
1579
1935
  skill.updated_at = saved.updated_at;
1936
+ if (native) needsRefresh = true;
1580
1937
  deactivated += agents.size;
1581
1938
  } catch (error) {
1582
1939
  needsRefresh = true;
@@ -1587,6 +1944,7 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
1587
1944
  if (needsRefresh) {
1588
1945
  const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1589
1946
  assertSkillsPullAuthorized(refreshed);
1947
+ assertSyncEnvironment(refreshed, pullResponse);
1590
1948
  Object.assign(pullResponse, refreshed);
1591
1949
  }
1592
1950
  return deactivated;
@@ -1606,11 +1964,12 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1606
1964
  "Cannot sync skills without a server-verified account identity."
1607
1965
  );
1608
1966
  }
1609
- const syncPaths = getSkillSyncPathsForUser(syncUserId);
1610
- const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
1611
- syncPaths.skillsDir
1612
- );
1967
+ const syncPaths = getSkillSyncPathsForUser(syncUserId, void 0, syncSettings.sync_namespace, options.syncHome);
1613
1968
  if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
1969
+ const foreignLinksRemoved2 = await deps.removeForeignAccountSymlinks(
1970
+ syncPaths.skillsDir,
1971
+ isolatedLinkOptions(syncPaths)
1972
+ );
1614
1973
  return {
1615
1974
  syncEnabled: false,
1616
1975
  pushed: 0,
@@ -1619,7 +1978,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1619
1978
  deleted: 0,
1620
1979
  deactivated: 0,
1621
1980
  linked: 0,
1622
- removed: foreignLinksRemoved,
1981
+ removed: foreignLinksRemoved2,
1623
1982
  skipped: 0,
1624
1983
  lastSyncedAt: syncSettings.last_synced_at,
1625
1984
  failedPushes: []
@@ -1627,13 +1986,18 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1627
1986
  }
1628
1987
  let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1629
1988
  assertSkillsPullAuthorized(pullResponse);
1989
+ assertSyncEnvironment(pullResponse, syncSettings);
1990
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
1991
+ syncPaths.skillsDir,
1992
+ isolatedLinkOptions(syncPaths)
1993
+ );
1630
1994
  const cloudCuratedSkillNames = new Set(
1631
1995
  pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
1632
1996
  );
1633
1997
  const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
1634
1998
  const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
1635
1999
  const assignmentFailures = [];
1636
- const deactivated = syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
2000
+ const deactivated = !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
1637
2001
  serverUrl,
1638
2002
  jwt,
1639
2003
  pullResponse,
@@ -1641,41 +2005,55 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1641
2005
  scopedState,
1642
2006
  syncPaths.skillsDir,
1643
2007
  deps,
1644
- assignmentFailures
2008
+ assignmentFailures,
2009
+ isolatedLinkOptions(syncPaths)
1645
2010
  ) : 0;
2011
+ assertSyncEnvironment(pullResponse, syncSettings);
1646
2012
  const authUserId = decodeJwtSubject(jwt);
1647
2013
  let previousAuthState = null;
1648
- if (authUserId && authUserId !== syncUserId) {
1649
- const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
2014
+ if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
2015
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId, void 0, void 0, options.syncHome);
1650
2016
  previousAuthState = await deps.readSyncState(previousAuthPaths);
1651
2017
  await deps.gatherTopLevelLocalSkills(syncPaths, {
1652
2018
  sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
1653
2019
  protectedSkillNames
1654
2020
  });
1655
2021
  }
1656
- await deps.gatherTopLevelLocalSkills(syncPaths, {
1657
- protectedSkillNames
1658
- });
2022
+ if (!syncPaths.isolatedNamespace) {
2023
+ await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
2024
+ }
1659
2025
  const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1660
2026
  const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
1661
2027
  localSkills,
1662
2028
  scopedState,
1663
- isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
2029
+ !syncPaths.isolatedNamespace && isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
1664
2030
  ));
2031
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
1665
2032
  const gatheredSymlinkResult = await deps.syncSymlinks(
1666
2033
  buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
1667
- syncPaths.skillsDir
2034
+ syncPaths.skillsDir,
2035
+ isolatedLinkOptions(syncPaths)
1668
2036
  );
1669
2037
  const pushCandidates = getPushCandidates(
1670
2038
  localSkills,
1671
2039
  previousState,
1672
2040
  cloudCuratedSkillNames,
1673
2041
  new Set(pullResponse.skills.map((skill) => skill.name)),
1674
- new Set(pullResponse.skills.filter((skill) => skill.app_owned || skill.owner_app_id).map((skill) => skill.name))
2042
+ pullResponse.skills
1675
2043
  );
1676
2044
  const failedPushes = [];
2045
+ const acknowledgedNative = /* @__PURE__ */ new Map();
2046
+ let writeLocalSkills = localSkills;
1677
2047
  if (pushCandidates.length > 0) {
1678
2048
  const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
2049
+ for (const result of pushResult.skills || []) {
2050
+ const saved = result;
2051
+ const candidate = pushCandidates.find((local) => local.nativeReference && local.cloudId === saved.id);
2052
+ if (candidate && saved.native_reference) {
2053
+ checkedNativeReference(saved.native_reference, candidate.cloudId);
2054
+ acknowledgedNative.set(candidate.cloudId, candidate.folderHash);
2055
+ }
2056
+ }
1679
2057
  if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
1680
2058
  failedPushes.push(...pushResult.failed);
1681
2059
  console.warn(
@@ -1684,13 +2062,21 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1684
2062
  }
1685
2063
  pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1686
2064
  assertSkillsPullAuthorized(pullResponse);
2065
+ assertSyncEnvironment(pullResponse, syncSettings);
2066
+ writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
2067
+ pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
1687
2068
  }
1688
2069
  const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
1689
2070
  let deleted = 0;
1690
2071
  for (const skillName of Object.keys(previousState.skills)) {
1691
2072
  if (!cloudSkillNames.has(skillName)) {
2073
+ const previous = previousState.skills[skillName];
2074
+ if (previous.nativeReference || previous.nativeUnavailable || pullResponse.skills.some((skill) => isNativeSkill(skill) && skill.id === previous.cloudId)) {
2075
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
2076
+ continue;
2077
+ }
1692
2078
  await deps.deleteLocalSkill(skillName, syncPaths);
1693
- await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
2079
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
1694
2080
  deleted += 1;
1695
2081
  }
1696
2082
  }
@@ -1699,18 +2085,20 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1699
2085
  const appliedFilesByName = {};
1700
2086
  const downloaded = await writePulledSkillsToScopedMirror(
1701
2087
  pullResponse,
1702
- localSkills,
2088
+ writeLocalSkills,
1703
2089
  previousState,
1704
2090
  syncPaths,
1705
2091
  deps,
1706
2092
  failedDownloads,
1707
2093
  writtenSkillNames,
1708
- appliedFilesByName
2094
+ appliedFilesByName,
2095
+ acknowledgedNative
1709
2096
  );
1710
2097
  const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1711
2098
  const symlinkResult = await deps.syncSymlinks(
1712
2099
  buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
1713
- syncPaths.skillsDir
2100
+ syncPaths.skillsDir,
2101
+ isolatedLinkOptions(syncPaths)
1714
2102
  );
1715
2103
  const verifiedLinks = { ...symlinkResult.verifiedAgentLinks ?? {} };
1716
2104
  for (const failure of failedDownloads) delete verifiedLinks[failure.name];
@@ -1723,7 +2111,8 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1723
2111
  verifiedLinks,
1724
2112
  new Set(failedDownloads.map((item) => item.name)),
1725
2113
  collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
1726
- collectAppliedFiles(pullResponse, previousState, appliedFilesByName)
2114
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
2115
+ previousState
1727
2116
  ),
1728
2117
  syncPaths
1729
2118
  );
@@ -1746,6 +2135,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1746
2135
  }
1747
2136
  export {
1748
2137
  fetchSyncSettings,
2138
+ isolatedLinkOptions,
1749
2139
  materializeCloudSkillsForLocalShell,
1750
2140
  runSkillSync,
1751
2141
  shouldWriteCloudSkill