@notis_ai/cli 0.2.0-beta.19.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 (216) hide show
  1. package/README.md +822 -135
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_boundary_rules.json +50 -0
  5. package/config/notis_app_design_rules.json +135 -0
  6. package/config/portal-route-roots.json +48 -0
  7. package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
  8. package/dist/base-skills/notis-apps/SKILL.md +93 -0
  9. package/dist/base-skills/notis-apps/references/architecture.md +114 -0
  10. package/dist/base-skills/notis-apps/references/context.md +81 -0
  11. package/dist/base-skills/notis-apps/references/design.md +97 -0
  12. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  13. package/dist/base-skills/notis-apps/references/reading.md +67 -0
  14. package/dist/base-skills/notis-apps/references/release.md +107 -0
  15. package/dist/base-skills/notis-apps/references/sdk.md +116 -0
  16. package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
  17. package/dist/base-skills/notis-apps/references/views.md +142 -0
  18. package/dist/base-skills/notis-cli/SKILL.md +154 -0
  19. package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
  20. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  21. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  22. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  23. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  24. package/dist/base-skills/notis-query/SKILL.md +41 -0
  25. package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
  26. package/dist/base-skills/notis-query/references/documents.md +63 -0
  27. package/dist/base-skills/notis-query/references/query.md +76 -0
  28. package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
  29. package/dist/sdk/src/agentContext.ts +36 -0
  30. package/dist/sdk/src/bundleModule.ts +79 -0
  31. package/dist/sdk/src/components/Dialog.tsx +46 -0
  32. package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
  33. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  34. package/dist/sdk/src/components/Markdown.tsx +60 -0
  35. package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
  36. package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  37. package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  38. package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  39. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  40. package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  41. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  42. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  43. package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
  44. package/dist/sdk/src/components/Skeleton.tsx +24 -0
  45. package/dist/sdk/src/config.ts +239 -0
  46. package/dist/sdk/src/documents.ts +258 -0
  47. package/dist/sdk/src/hooks/useAction.ts +28 -0
  48. package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
  49. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  50. package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
  51. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  52. package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
  53. package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  54. package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  55. package/dist/sdk/src/hooks/useDocument.ts +43 -0
  56. package/dist/sdk/src/hooks/useDocuments.ts +84 -0
  57. package/dist/sdk/src/hooks/useHandover.ts +78 -0
  58. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  59. package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
  60. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  61. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  62. package/dist/sdk/src/hooks/useNotis.ts +42 -0
  63. package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
  64. package/dist/sdk/src/hooks/useQuery.ts +74 -0
  65. package/dist/sdk/src/hooks/useTool.ts +65 -0
  66. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  67. package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
  68. package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
  69. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  70. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  71. package/dist/sdk/src/index.ts +200 -0
  72. package/dist/sdk/src/interactions/actions.ts +59 -0
  73. package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
  74. package/dist/sdk/src/interactions/visibility.ts +13 -0
  75. package/dist/sdk/src/interactions.ts +45 -0
  76. package/dist/sdk/src/nativeData.ts +39 -0
  77. package/dist/sdk/src/presentation.tsx +82 -0
  78. package/dist/sdk/src/provider.tsx +49 -0
  79. package/dist/sdk/src/queryCache.ts +172 -0
  80. package/dist/sdk/src/queryErrors.ts +51 -0
  81. package/dist/sdk/src/runtime.ts +603 -0
  82. package/dist/sdk/src/space.ts +425 -0
  83. package/dist/sdk/src/styles.css +290 -0
  84. package/dist/sdk/src/tailwind.ts +66 -0
  85. package/dist/sdk/src/vite.ts +83 -0
  86. package/dist/skill-sync/index.js +2143 -0
  87. package/dist/skill-sync/index.js.map +7 -0
  88. package/dist/skill-sync-worker.mjs +3487 -0
  89. package/dist/space-harness/host.js +66 -0
  90. package/dist/view-renderer/dist/host.css +1 -0
  91. package/dist/view-renderer/dist/host.js +1042 -0
  92. package/dist/view-renderer/dist/theme.css +73 -0
  93. package/dist/view-renderer/src/dom.js +180 -0
  94. package/dist/view-renderer/src/errors.js +8 -0
  95. package/dist/view-renderer/src/host.tsx +173 -0
  96. package/dist/view-renderer/src/index.js +4 -0
  97. package/dist/view-renderer/src/pool.js +76 -0
  98. package/dist/view-renderer/src/remote-broker.js +59 -0
  99. package/dist/view-renderer/src/renderer.js +201 -0
  100. package/package.json +25 -8
  101. package/skills/notis-apps/cli.md +406 -0
  102. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  103. package/skills/notis-onboarding/BRIEF.md +133 -0
  104. package/skills/notis-query/cli.md +39 -0
  105. package/src/agent-hook-entry.js +5 -0
  106. package/src/cli.js +294 -25
  107. package/src/command-specs/agents.js +392 -0
  108. package/src/command-specs/apps.js +1476 -203
  109. package/src/command-specs/auth.js +114 -137
  110. package/src/command-specs/diagnostics.js +730 -0
  111. package/src/command-specs/files.js +29 -0
  112. package/src/command-specs/handover.js +374 -0
  113. package/src/command-specs/helpers.js +86 -82
  114. package/src/command-specs/index.js +31 -6
  115. package/src/command-specs/meta.js +150 -18
  116. package/src/command-specs/onboarding.js +290 -0
  117. package/src/command-specs/profile.js +358 -0
  118. package/src/command-specs/skills.js +304 -0
  119. package/src/command-specs/smoke.js +386 -0
  120. package/src/command-specs/spaces.js +572 -0
  121. package/src/command-specs/tools.js +455 -139
  122. package/src/command-specs/views.js +69 -0
  123. package/src/runtime/agent-browser.js +700 -0
  124. package/src/runtime/agent-memory-state.js +126 -0
  125. package/src/runtime/agent-setup.js +383 -0
  126. package/src/runtime/app-boundary-validator.js +407 -0
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-data-literals.js +82 -0
  129. package/src/runtime/app-platform.js +2752 -237
  130. package/src/runtime/app-registry-scaffolds.js +367 -0
  131. package/src/runtime/app-test-server.js +293 -0
  132. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  133. package/src/runtime/auth-recovery.js +110 -0
  134. package/src/runtime/base-skills.d.ts +20 -0
  135. package/src/runtime/base-skills.js +167 -0
  136. package/src/runtime/channel.js +133 -0
  137. package/src/runtime/confined-space-build.js +74 -0
  138. package/src/runtime/confined-space-verify.js +49 -0
  139. package/src/runtime/delegated-context.js +68 -0
  140. package/src/runtime/errors.js +1 -0
  141. package/src/runtime/git.js +233 -0
  142. package/src/runtime/login-listener.js +15 -0
  143. package/src/runtime/native-record-files.js +47 -0
  144. package/src/runtime/oauth.js +2622 -0
  145. package/src/runtime/output.js +37 -5
  146. package/src/runtime/ports.js +31 -0
  147. package/src/runtime/profiles.js +906 -55
  148. package/src/runtime/retired-sdk-files.js +20 -0
  149. package/src/runtime/screenshot-framing.js +85 -0
  150. package/src/runtime/skill-sync/cloud-client.ts +121 -0
  151. package/src/runtime/skill-sync/index.ts +905 -0
  152. package/src/runtime/skill-sync/local-scanner.ts +1259 -0
  153. package/src/runtime/skill-sync/native-identity.ts +102 -0
  154. package/src/runtime/skill-sync/native-plan.ts +59 -0
  155. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  156. package/src/runtime/skill-sync/sync-plan.ts +54 -0
  157. package/src/runtime/skill-sync/types.ts +157 -0
  158. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  159. package/src/runtime/skill-sync-service.js +109 -0
  160. package/src/runtime/source-root.js +32 -0
  161. package/src/runtime/space-collection-scaffold.js +66 -0
  162. package/src/runtime/space-database-structures.js +229 -0
  163. package/src/runtime/space-fixture-broker.js +50 -0
  164. package/src/runtime/space-lock.js +337 -0
  165. package/src/runtime/space-platform.js +105 -0
  166. package/src/runtime/space-source.js +281 -0
  167. package/src/runtime/space-test-server.js +327 -0
  168. package/src/runtime/space-verification.js +89 -0
  169. package/src/runtime/space-verify-browser-worker.js +51 -0
  170. package/src/runtime/space-view-manifest.js +254 -0
  171. package/src/runtime/store-screenshot.js +146 -0
  172. package/src/runtime/sync-skills.d.ts +37 -0
  173. package/src/runtime/sync-skills.js +231 -0
  174. package/src/runtime/telemetry.js +92 -0
  175. package/src/runtime/transport.js +331 -45
  176. package/src/runtime/view-render.js +49 -0
  177. package/src/skill-sync-worker-entry.js +2 -0
  178. package/src/skill-sync-worker.js +50 -0
  179. package/src/space-harness-host.ts +103 -0
  180. package/template/.harness/index.html.tmpl +435 -0
  181. package/template/CHANGELOG.md +5 -0
  182. package/template/app/globals.css +29 -3
  183. package/template/app/layout.tsx +6 -3
  184. package/template/app/page.tsx +49 -42
  185. package/template/components/page-heading.tsx +23 -0
  186. package/template/components/ui/badge.tsx +7 -4
  187. package/template/components/ui/button.tsx +1 -1
  188. package/template/components/ui/card.tsx +24 -11
  189. package/template/components/ui/native-select.tsx +24 -0
  190. package/template/notis.config.ts +24 -6
  191. package/template/package-lock.json +3642 -0
  192. package/template/package.json +19 -16
  193. package/template/postcss.config.mjs +1 -1
  194. package/template/tailwind.config.ts +1 -6
  195. package/template/tsconfig.json +1 -0
  196. package/src/command-specs/db.js +0 -163
  197. package/src/runtime/app-preview-server.js +0 -312
  198. package/template/packages/notis-sdk/src/config.ts +0 -48
  199. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  200. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  201. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  202. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  203. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  204. package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
  205. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
  206. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  207. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  208. package/template/packages/notis-sdk/src/index.ts +0 -47
  209. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  210. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  211. package/template/packages/notis-sdk/src/styles.css +0 -123
  212. package/template/packages/notis-sdk/src/vite.ts +0 -54
  213. package/template/packages/notis-sdk/tsconfig.json +0 -15
  214. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
  215. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
  216. /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
@@ -0,0 +1,2143 @@
1
+ // src/runtime/skill-sync/index.ts
2
+ import { createHash as createHash4 } from "node:crypto";
3
+ import { join } from "node:path";
4
+
5
+ // src/runtime/skill-sync/types.ts
6
+ var DEFAULT_AGENT_TARGETS = {
7
+ notis: true,
8
+ claude_code: true,
9
+ cursor: true,
10
+ codex: true
11
+ };
12
+ function normalizeAgentTargets(targets) {
13
+ return {
14
+ notis: Boolean(targets?.notis ?? DEFAULT_AGENT_TARGETS.notis),
15
+ claude_code: Boolean(targets?.claude_code ?? DEFAULT_AGENT_TARGETS.claude_code),
16
+ cursor: Boolean(targets?.cursor ?? DEFAULT_AGENT_TARGETS.cursor),
17
+ codex: Boolean(targets?.codex ?? DEFAULT_AGENT_TARGETS.codex)
18
+ };
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
+
138
+ // src/runtime/skill-sync/local-scanner.ts
139
+ import { createHash as createHash2 } from "crypto";
140
+ import { execFile } from "child_process";
141
+ import { promises as fs } from "fs";
142
+ import os from "os";
143
+ import path from "path";
144
+ import { promisify } from "util";
145
+ var HOME_DIR = os.homedir();
146
+ var execFileAsync = promisify(execFile);
147
+ var NativeSkillIdentityError = class extends Error {
148
+ };
149
+ var AGENTS_DIR = path.join(HOME_DIR, ".agents");
150
+ var LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
151
+ var NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
152
+ var LEGACY_NOTIS_SYNC_STATE_PATH = path.join(
153
+ AGENTS_DIR,
154
+ ".notis-sync.json"
155
+ );
156
+ var AGENTS_SKILLS_DIR = LEGACY_AGENTS_SKILLS_DIR;
157
+ var SKILL_LOCK_PATH = path.join(AGENTS_DIR, ".skill-lock.json");
158
+ var DEFAULT_SYNC_STATE = {
159
+ version: 1,
160
+ lastSyncedAt: null,
161
+ skills: {}
162
+ };
163
+ var EXCLUDED_TOP_LEVEL_ROOT_NAMES = /* @__PURE__ */ new Set([
164
+ "backup",
165
+ "backups",
166
+ "builtin",
167
+ "builtins",
168
+ "cache",
169
+ "caches",
170
+ "marketplace",
171
+ "marketplaces",
172
+ "plugin",
173
+ "plugins",
174
+ "temp",
175
+ "tmp",
176
+ "worktree",
177
+ "worktrees"
178
+ ]);
179
+ var DEFAULT_SYNC_PATHS = {
180
+ agentsDir: AGENTS_DIR,
181
+ syncRoot: NOTIS_SKILL_SYNC_ROOT,
182
+ legacySkillsDir: LEGACY_AGENTS_SKILLS_DIR,
183
+ legacyScopedSkillsDir: LEGACY_AGENTS_SKILLS_DIR,
184
+ legacyScopedSyncStatePath: LEGACY_NOTIS_SYNC_STATE_PATH,
185
+ skillsDir: LEGACY_AGENTS_SKILLS_DIR,
186
+ syncStatePath: LEGACY_NOTIS_SYNC_STATE_PATH,
187
+ skillLockPath: SKILL_LOCK_PATH,
188
+ gatherMetadataPath: path.join(NOTIS_SKILL_SYNC_ROOT, "skill-gather-metadata.json")
189
+ };
190
+ function getDefaultSyncRootForAgentsDir(resolvedAgentsDir) {
191
+ if (resolvedAgentsDir === path.resolve(AGENTS_DIR)) {
192
+ return NOTIS_SKILL_SYNC_ROOT;
193
+ }
194
+ return path.join(resolvedAgentsDir, ".notis", "skills");
195
+ }
196
+ function isResolvedChildPath(baseDir, candidatePath) {
197
+ return candidatePath.startsWith(`${baseDir}${path.sep}`);
198
+ }
199
+ function sanitizePathSegment(value) {
200
+ const raw = value.trim();
201
+ const sanitized = raw.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
202
+ if (!sanitized) {
203
+ throw new Error("Invalid sync user id");
204
+ }
205
+ if (sanitized === raw) {
206
+ return sanitized;
207
+ }
208
+ return `${sanitized}-${createHash2("sha256").update(raw).digest("hex").slice(0, 12)}`;
209
+ }
210
+ function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR, namespace, syncHome) {
211
+ const safeUserId = sanitizePathSegment(authUserId);
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);
216
+ const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
217
+ return {
218
+ ...homeDir ? { homeDir } : {},
219
+ ...namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {},
220
+ agentsDir: resolvedAgentsDir,
221
+ syncRoot,
222
+ legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
223
+ legacyScopedSkillsDir: path.join(legacyUserRoot, "skills"),
224
+ legacyScopedSyncStatePath: path.join(legacyUserRoot, ".notis-sync.json"),
225
+ skillsDir: path.join(userRoot, "skills"),
226
+ syncStatePath: path.join(userRoot, ".notis-sync.json"),
227
+ skillLockPath: path.join(resolvedAgentsDir, ".skill-lock.json"),
228
+ gatherMetadataPath: path.join(userRoot, ".notis-gathered-skills.json")
229
+ };
230
+ }
231
+ function assertRelativeBundlePath(relativePath) {
232
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
233
+ if (!normalized || normalized.startsWith("../") || `/${normalized}/`.includes("/../")) {
234
+ throw new Error(`Invalid bundle file path: ${relativePath}`);
235
+ }
236
+ return normalized;
237
+ }
238
+ function stripWrappingQuotes(value) {
239
+ return value.replace(/^['"]|['"]$/g, "").trim();
240
+ }
241
+ function parseFrontMatter(skillMd) {
242
+ const match = skillMd.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
243
+ if (!match) {
244
+ return { description: "" };
245
+ }
246
+ let description = "";
247
+ for (const line of match[1].split("\n")) {
248
+ const trimmed = line.trim();
249
+ if (!trimmed || trimmed.startsWith("#")) {
250
+ continue;
251
+ }
252
+ const descriptionMatch = trimmed.match(/^description\s*:\s*(.+)$/i);
253
+ if (descriptionMatch) {
254
+ description = stripWrappingQuotes(descriptionMatch[1]);
255
+ break;
256
+ }
257
+ }
258
+ return { description };
259
+ }
260
+ async function readJsonFile(filePath) {
261
+ try {
262
+ const raw = await fs.readFile(filePath, "utf8");
263
+ return JSON.parse(raw);
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+ var IGNORED_SKILL_DIRECTORIES = /* @__PURE__ */ new Set(["__pycache__", ".pytest_cache"]);
269
+ function isIgnoredSkillEntry(entry) {
270
+ if (entry.name === ".DS_Store") return true;
271
+ if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
272
+ return /\.py[co]$/i.test(entry.name);
273
+ }
274
+ async function listFilesRecursive(dirPath, rootPath = dirPath, strictLinks = false) {
275
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
276
+ const nested = await Promise.all(
277
+ entries.map(async (entry) => {
278
+ if (isIgnoredSkillEntry(entry) || dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE) {
279
+ return [];
280
+ }
281
+ const fullPath = path.join(dirPath, entry.name);
282
+ if (strictLinks && entry.isSymbolicLink()) throw new Error("Native Skill bundles cannot contain symbolic links.");
283
+ if (entry.isDirectory()) {
284
+ return listFilesRecursive(fullPath, rootPath, strictLinks);
285
+ }
286
+ if (entry.isFile()) {
287
+ return [fullPath];
288
+ }
289
+ return [];
290
+ })
291
+ );
292
+ return nested.flat().sort();
293
+ }
294
+ async function computeFolderHash(dirPath) {
295
+ const hash = createHash2("sha256");
296
+ const filePaths = await listFilesRecursive(dirPath);
297
+ for (const filePath of filePaths) {
298
+ const relativePath = path.relative(dirPath, filePath);
299
+ hash.update(relativePath);
300
+ hash.update("\0");
301
+ hash.update(await fs.readFile(filePath));
302
+ hash.update("\0");
303
+ }
304
+ return hash.digest("hex");
305
+ }
306
+ function resolveSkillBundleFilePath(skillDir, relativePath) {
307
+ const normalizedPath = assertRelativeBundlePath(relativePath);
308
+ const resolvedSkillDir = path.resolve(skillDir);
309
+ const candidatePath = path.resolve(resolvedSkillDir, normalizedPath);
310
+ if (candidatePath === resolvedSkillDir || !isResolvedChildPath(resolvedSkillDir, candidatePath)) {
311
+ throw new Error(
312
+ `Bundle file resolves outside the expected directory: ${relativePath}`
313
+ );
314
+ }
315
+ return candidatePath;
316
+ }
317
+ async function readSkillMdIfValid(skillDir) {
318
+ try {
319
+ await fs.readFile(path.join(skillDir, "SKILL.md"), "utf8");
320
+ return true;
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
325
+ async function moveDirectory(sourceDir, destinationDir) {
326
+ await fs.mkdir(path.dirname(destinationDir), { recursive: true });
327
+ try {
328
+ await fs.rename(sourceDir, destinationDir);
329
+ } catch (error) {
330
+ if (error.code !== "EXDEV") {
331
+ throw error;
332
+ }
333
+ await fs.cp(sourceDir, destinationDir, {
334
+ recursive: true,
335
+ errorOnExist: true,
336
+ force: false
337
+ });
338
+ await fs.rm(sourceDir, { recursive: true, force: true });
339
+ }
340
+ }
341
+ async function createDirectorySymlink(targetDir, linkPath) {
342
+ await fs.mkdir(path.dirname(linkPath), { recursive: true });
343
+ await fs.symlink(path.relative(path.dirname(linkPath), targetDir), linkPath, "dir");
344
+ }
345
+ function backupRootFor(paths, timestamp) {
346
+ return path.join(paths.syncRoot, "skill-dedupe-backups", timestamp);
347
+ }
348
+ function relativeBackupPath(label, skillName) {
349
+ return path.join(
350
+ label.replace(/[^A-Za-z0-9_.-]+/g, "_"),
351
+ safeName(skillName)
352
+ );
353
+ }
354
+ function isExcludedTopLevelSkillEntry(entryName) {
355
+ return entryName.startsWith(".") || isTransientSkillDirectoryName(entryName) || EXCLUDED_TOP_LEVEL_ROOT_NAMES.has(entryName.toLowerCase());
356
+ }
357
+ function isTransientSkillDirectoryName(entryName) {
358
+ return /\.(?:backup|staging)-/.test(entryName);
359
+ }
360
+ function defaultTopLevelSkillSources(paths) {
361
+ const sources = [];
362
+ if (path.resolve(paths.legacyScopedSkillsDir) !== path.resolve(paths.skillsDir)) {
363
+ sources.push({
364
+ label: "notis-legacy-scoped",
365
+ root: paths.legacyScopedSkillsDir,
366
+ priority: 1
367
+ });
368
+ }
369
+ sources.push(
370
+ { label: "agents", root: paths.legacySkillsDir, priority: 2 },
371
+ {
372
+ label: "codex",
373
+ root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
374
+ priority: 3
375
+ },
376
+ {
377
+ label: "cursor",
378
+ root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
379
+ priority: 4
380
+ },
381
+ {
382
+ label: "claude",
383
+ root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
384
+ priority: 5
385
+ }
386
+ );
387
+ return sources;
388
+ }
389
+ function isManagedTopLevelSymlinkTarget(resolvedPath, paths) {
390
+ const managedRoots = [
391
+ path.resolve(paths.skillsDir),
392
+ path.resolve(paths.legacySkillsDir),
393
+ path.resolve(paths.legacyScopedSkillsDir),
394
+ path.join(path.resolve(paths.syncRoot), "base"),
395
+ path.join(path.resolve(paths.syncRoot), "users"),
396
+ path.join(path.resolve(paths.agentsDir), "notis", "users")
397
+ ];
398
+ return managedRoots.some(
399
+ (root) => resolvedPath === root || resolvedPath.startsWith(`${root}${path.sep}`)
400
+ );
401
+ }
402
+ async function listTopLevelSkillCandidates(paths, options) {
403
+ const sourceRoots = options.sourceRoots ? options.sourceRoots.map((source, index) => ({
404
+ ...source,
405
+ priority: index + 1
406
+ })) : defaultTopLevelSkillSources(paths);
407
+ const candidates = [];
408
+ try {
409
+ const scopedEntries = await fs.readdir(paths.skillsDir, {
410
+ withFileTypes: true
411
+ });
412
+ for (const entry of scopedEntries) {
413
+ if (!entry.isDirectory() && !entry.isSymbolicLink() || entry.name.startsWith(".") || isTransientSkillDirectoryName(entry.name)) {
414
+ continue;
415
+ }
416
+ const skillDir = path.join(paths.skillsDir, entry.name);
417
+ let resolvedPath = path.resolve(skillDir);
418
+ if (entry.isSymbolicLink()) {
419
+ try {
420
+ resolvedPath = path.resolve(path.dirname(skillDir), await fs.readlink(skillDir));
421
+ } catch {
422
+ continue;
423
+ }
424
+ }
425
+ if (await readSkillMdIfValid(skillDir)) {
426
+ candidates.push({
427
+ name: safeName(entry.name, paths.skillsDir),
428
+ root: paths.skillsDir,
429
+ label: "notis-managed",
430
+ path: skillDir,
431
+ resolvedPath,
432
+ priority: 0,
433
+ isScoped: true,
434
+ isSymlink: entry.isSymbolicLink()
435
+ });
436
+ }
437
+ }
438
+ } catch {
439
+ }
440
+ for (const source of sourceRoots) {
441
+ let entries;
442
+ try {
443
+ entries = await fs.readdir(source.root, { withFileTypes: true });
444
+ } catch {
445
+ continue;
446
+ }
447
+ for (const entry of entries) {
448
+ if (isExcludedTopLevelSkillEntry(entry.name) || !entry.isDirectory() && !entry.isSymbolicLink()) {
449
+ continue;
450
+ }
451
+ const candidatePath = path.join(source.root, entry.name);
452
+ let resolvedPath;
453
+ try {
454
+ resolvedPath = entry.isSymbolicLink() ? path.resolve(
455
+ path.dirname(candidatePath),
456
+ await fs.readlink(candidatePath)
457
+ ) : path.resolve(candidatePath);
458
+ } catch {
459
+ continue;
460
+ }
461
+ if (entry.isSymbolicLink() && isManagedTopLevelSymlinkTarget(resolvedPath, paths)) {
462
+ continue;
463
+ }
464
+ if (!await readSkillMdIfValid(resolvedPath)) {
465
+ continue;
466
+ }
467
+ candidates.push({
468
+ name: safeName(entry.name, paths.skillsDir),
469
+ root: source.root,
470
+ label: source.label,
471
+ path: candidatePath,
472
+ resolvedPath,
473
+ priority: source.priority,
474
+ isScoped: false,
475
+ isSymlink: entry.isSymbolicLink()
476
+ });
477
+ }
478
+ }
479
+ return candidates.sort(
480
+ (a, b) => a.priority - b.priority || a.name.localeCompare(b.name)
481
+ );
482
+ }
483
+ async function gatherTopLevelLocalSkills(paths, options = {}) {
484
+ await ensureCanonicalSkillsDir(paths);
485
+ const protectedSkillNames = options.protectedSkillNames || /* @__PURE__ */ new Set();
486
+ const timestamp = options.timestamp || (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
487
+ const backupRoot = backupRootFor(paths, timestamp);
488
+ const candidates = await listTopLevelSkillCandidates(paths, options);
489
+ const byName = /* @__PURE__ */ new Map();
490
+ for (const candidate of candidates) {
491
+ const existing = byName.get(candidate.name) || [];
492
+ existing.push(candidate);
493
+ byName.set(candidate.name, existing);
494
+ }
495
+ let gathered = 0;
496
+ let backedUp = 0;
497
+ let skipped = 0;
498
+ const metadata = {
499
+ version: 1,
500
+ gatheredAt: (/* @__PURE__ */ new Date()).toISOString(),
501
+ skills: {}
502
+ };
503
+ for (const [skillName, skillCandidates] of byName) {
504
+ const protectedFromLocalGather = protectedSkillNames.has(skillName);
505
+ const canonical = protectedFromLocalGather ? skillCandidates.find((candidate) => candidate.isScoped) || null : skillCandidates[0];
506
+ const destinationName = safeName(skillName, paths.skillsDir);
507
+ const destinationDir = path.join(paths.skillsDir, destinationName);
508
+ const skippedSources = [];
509
+ if (!canonical) {
510
+ for (const candidate of skillCandidates) {
511
+ if (candidate.isSymlink) {
512
+ skipped += 1;
513
+ skippedSources.push(candidate.path);
514
+ continue;
515
+ }
516
+ const backupDir = path.join(
517
+ backupRoot,
518
+ relativeBackupPath(candidate.label, skillName)
519
+ );
520
+ try {
521
+ await moveDirectory(candidate.path, backupDir);
522
+ backedUp += 1;
523
+ skippedSources.push(candidate.path);
524
+ } catch {
525
+ skipped += 1;
526
+ }
527
+ }
528
+ metadata.skills[skillName] = {
529
+ canonicalPath: null,
530
+ skippedSources,
531
+ protectedFromLocalGather: true
532
+ };
533
+ continue;
534
+ }
535
+ if (!canonical.isScoped && !await pathExists(destinationDir)) {
536
+ try {
537
+ if (canonical.isSymlink) {
538
+ await createDirectorySymlink(canonical.resolvedPath, destinationDir);
539
+ } else {
540
+ await moveDirectory(canonical.path, destinationDir);
541
+ }
542
+ gathered += 1;
543
+ } catch {
544
+ skipped += 1;
545
+ }
546
+ }
547
+ for (const candidate of skillCandidates) {
548
+ if (candidate === canonical || candidate.isScoped) {
549
+ continue;
550
+ }
551
+ if (candidate.isSymlink) {
552
+ skippedSources.push(candidate.path);
553
+ continue;
554
+ }
555
+ const backupDir = path.join(
556
+ backupRoot,
557
+ relativeBackupPath(candidate.label, skillName)
558
+ );
559
+ try {
560
+ await moveDirectory(candidate.path, backupDir);
561
+ backedUp += 1;
562
+ skippedSources.push(candidate.path);
563
+ } catch {
564
+ skipped += 1;
565
+ }
566
+ }
567
+ metadata.skills[skillName] = {
568
+ canonicalPath: await pathExists(destinationDir) ? destinationDir : canonical.path,
569
+ skippedSources,
570
+ ...protectedFromLocalGather ? { protectedFromLocalGather: true } : {}
571
+ };
572
+ }
573
+ await fs.mkdir(path.dirname(paths.gatherMetadataPath), { recursive: true });
574
+ await fs.writeFile(
575
+ paths.gatherMetadataPath,
576
+ `${JSON.stringify(metadata, null, 2)}
577
+ `,
578
+ "utf8"
579
+ );
580
+ return {
581
+ gathered,
582
+ backedUp,
583
+ skipped,
584
+ metadataPath: paths.gatherMetadataPath
585
+ };
586
+ }
587
+ async function readSkillLock(paths = DEFAULT_SYNC_PATHS) {
588
+ const lockData = await readJsonFile(paths.skillLockPath);
589
+ const sourceUrls = {};
590
+ for (const [skillName, entry] of Object.entries(lockData?.skills || {})) {
591
+ if (typeof entry?.sourceUrl === "string" && entry.sourceUrl.trim()) {
592
+ sourceUrls[skillName] = entry.sourceUrl.trim();
593
+ }
594
+ }
595
+ return sourceUrls;
596
+ }
597
+ async function ensureCanonicalSkillsDir(paths = DEFAULT_SYNC_PATHS) {
598
+ await fs.mkdir(paths.skillsDir, { recursive: true });
599
+ }
600
+ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
601
+ await ensureCanonicalSkillsDir(paths);
602
+ const sourceUrls = await readSkillLock(paths);
603
+ const previousState = await readSyncState(paths);
604
+ const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
605
+ const skills = await Promise.all(
606
+ entries.map(async (entry) => {
607
+ if (!entry.isDirectory() && !entry.isSymbolicLink() || isTransientSkillDirectoryName(entry.name)) {
608
+ return null;
609
+ }
610
+ const skillDir = path.join(paths.skillsDir, entry.name);
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));
621
+ try {
622
+ const skillMd = await fs.readFile(skillMdPath, "utf8");
623
+ const { description } = parseFrontMatter(skillMd);
624
+ const folderHash = await computeFolderHash(skillDir);
625
+ const skill = {
626
+ name: entry.name,
627
+ skillMd,
628
+ description,
629
+ folderHash,
630
+ directoryPath: skillDir
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
+ }
657
+ if (sourceUrls[entry.name]) {
658
+ skill.sourceUrl = sourceUrls[entry.name];
659
+ }
660
+ return skill;
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.");
664
+ return null;
665
+ }
666
+ })
667
+ );
668
+ return skills.filter((skill) => skill !== null).sort((a, b) => a.name.localeCompare(b.name));
669
+ }
670
+ function normalizeSyncState(state) {
671
+ if (!state || state.version !== 1 || typeof state.skills !== "object") {
672
+ return null;
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
+ }
677
+ const normalizeStoredAgentTargets = (targets) => ({
678
+ notis: Boolean(targets?.notis ?? true),
679
+ claude_code: Boolean(targets?.claude_code ?? true),
680
+ cursor: Boolean(targets?.cursor ?? true),
681
+ codex: Boolean(targets?.codex ?? true)
682
+ });
683
+ return {
684
+ version: 1,
685
+ ...state.nativeHistory ? { nativeHistory: state.nativeHistory } : {},
686
+ lastSyncedAt: typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
687
+ skills: Object.fromEntries(
688
+ Object.entries(state.skills || {}).map(([skillName, skillState]) => [
689
+ skillName,
690
+ {
691
+ ...skillState,
692
+ agentTargets: normalizeStoredAgentTargets(skillState.agentTargets)
693
+ }
694
+ ])
695
+ )
696
+ };
697
+ }
698
+ async function readSyncState(paths = DEFAULT_SYNC_PATHS) {
699
+ const scopedState = normalizeSyncState(
700
+ await readJsonFile(paths.syncStatePath)
701
+ );
702
+ if (scopedState) {
703
+ return scopedState;
704
+ }
705
+ return DEFAULT_SYNC_STATE;
706
+ }
707
+ async function readLegacySyncState(paths = DEFAULT_SYNC_PATHS) {
708
+ const legacyStatePaths = [
709
+ paths.legacyScopedSyncStatePath,
710
+ path.resolve(paths.syncStatePath) === path.resolve(LEGACY_NOTIS_SYNC_STATE_PATH) ? paths.syncStatePath : path.join(paths.agentsDir, ".notis-sync.json")
711
+ ];
712
+ for (const legacyStatePath of legacyStatePaths) {
713
+ const state = normalizeSyncState(
714
+ await readJsonFile(legacyStatePath)
715
+ );
716
+ if (state) {
717
+ return state;
718
+ }
719
+ }
720
+ return null;
721
+ }
722
+ async function writeSyncState(state, paths = DEFAULT_SYNC_PATHS) {
723
+ await fs.mkdir(path.dirname(paths.syncStatePath), { recursive: true });
724
+ await fs.writeFile(
725
+ paths.syncStatePath,
726
+ `${JSON.stringify(state, null, 2)}
727
+ `,
728
+ "utf8"
729
+ );
730
+ }
731
+ function safeName(name, baseDir = AGENTS_SKILLS_DIR) {
732
+ const sanitizedName = name.trim().replace(/[\\/]+/g, "").replace(/\.\./g, "");
733
+ if (!sanitizedName) {
734
+ throw new Error("Invalid skill name");
735
+ }
736
+ const resolvedBaseDir = path.resolve(baseDir);
737
+ const resolvedPath = path.resolve(resolvedBaseDir, sanitizedName);
738
+ if (!isResolvedChildPath(resolvedBaseDir, resolvedPath)) {
739
+ throw new Error("Skill name resolves outside the expected directory");
740
+ }
741
+ return sanitizedName;
742
+ }
743
+ async function deleteLocalSkill(skillName, paths = DEFAULT_SYNC_PATHS) {
744
+ const skillDir = path.join(
745
+ paths.skillsDir,
746
+ safeName(skillName, paths.skillsDir)
747
+ );
748
+ try {
749
+ await fs.rm(skillDir, { recursive: true, force: true });
750
+ return true;
751
+ } catch {
752
+ return false;
753
+ }
754
+ }
755
+ async function createZipFromDirectory(directoryPath) {
756
+ const zipPath = path.join(
757
+ os.tmpdir(),
758
+ `notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
759
+ );
760
+ const directoryName = path.basename(directoryPath);
761
+ const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "notis-skill-bundle-"));
762
+ try {
763
+ const stagedDir = path.join(stagingRoot, directoryName);
764
+ for (const filePath of await listFilesRecursive(directoryPath)) {
765
+ const target = path.join(stagedDir, path.relative(directoryPath, filePath));
766
+ await fs.mkdir(path.dirname(target), { recursive: true });
767
+ await fs.copyFile(filePath, target);
768
+ }
769
+ await fs.mkdir(stagedDir, { recursive: true });
770
+ if (process.platform === "win32") {
771
+ const systemTar = path.join(
772
+ process.env.SystemRoot || "C:\\Windows",
773
+ "System32",
774
+ "tar.exe"
775
+ );
776
+ if (await pathExists(systemTar)) {
777
+ await execFileAsync(systemTar, [
778
+ "-c",
779
+ "--format",
780
+ "zip",
781
+ "-f",
782
+ zipPath,
783
+ "-C",
784
+ stagingRoot,
785
+ directoryName
786
+ ]);
787
+ return zipPath;
788
+ }
789
+ await execFileAsync("powershell.exe", [
790
+ "-NoProfile",
791
+ "-Command",
792
+ `Compress-Archive -Path '${stagedDir.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`
793
+ ]);
794
+ return zipPath;
795
+ }
796
+ await execFileAsync("zip", ["-qry", zipPath, directoryName], {
797
+ cwd: stagingRoot
798
+ });
799
+ return zipPath;
800
+ } finally {
801
+ await fs.rm(stagingRoot, { recursive: true, force: true });
802
+ }
803
+ }
804
+ async function extractZipToDirectory(zipPath, destinationDir) {
805
+ const extractRoot = await fs.mkdtemp(
806
+ path.join(os.tmpdir(), "notis-skill-extract-")
807
+ );
808
+ try {
809
+ if (process.platform === "win32") {
810
+ await execFileAsync("powershell.exe", [
811
+ "-NoProfile",
812
+ "-Command",
813
+ `Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${extractRoot.replace(/'/g, "''")}' -Force`
814
+ ]);
815
+ } else {
816
+ await execFileAsync("unzip", ["-qq", zipPath, "-d", extractRoot]);
817
+ }
818
+ const extractedEntries = await fs.readdir(extractRoot, {
819
+ withFileTypes: true
820
+ });
821
+ const extractedDirectory = extractedEntries.find(
822
+ (entry) => entry.isDirectory()
823
+ );
824
+ const sourceDir = extractedDirectory ? path.join(extractRoot, extractedDirectory.name) : extractRoot;
825
+ await fs.rm(destinationDir, { recursive: true, force: true });
826
+ await fs.mkdir(path.dirname(destinationDir), { recursive: true });
827
+ await fs.cp(sourceDir, destinationDir, { recursive: true });
828
+ return await listRelativeFiles(destinationDir);
829
+ } finally {
830
+ await fs.rm(extractRoot, { recursive: true, force: true });
831
+ }
832
+ }
833
+ async function listRelativeFiles(dirPath) {
834
+ try {
835
+ const files = await listFilesRecursive(dirPath);
836
+ return files.map((filePath) => toPosixRelativePath(dirPath, filePath)).sort();
837
+ } catch (error) {
838
+ if (error?.code === "ENOENT") return [];
839
+ throw error;
840
+ }
841
+ }
842
+ function toPosixRelativePath(rootDir, filePath) {
843
+ return path.relative(rootDir, filePath).split(path.sep).join("/");
844
+ }
845
+ async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied, strictConflicts = false) {
846
+ const managed = previouslyApplied ? new Set(previouslyApplied) : null;
847
+ for (const existingFile of await listFilesRecursive(existingDir)) {
848
+ const relativePath = toPosixRelativePath(existingDir, existingFile);
849
+ if (managed?.has(relativePath)) continue;
850
+ const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
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
+ }
857
+ await fs.mkdir(path.dirname(stagedPath), { recursive: true });
858
+ await fs.copyFile(existingFile, stagedPath);
859
+ }
860
+ }
861
+ async function pathExists(targetPath) {
862
+ try {
863
+ await fs.access(targetPath);
864
+ return true;
865
+ } catch {
866
+ return false;
867
+ }
868
+ }
869
+ async function replaceSkillDirectoryAtomically(skillDir, populateDir, preserve) {
870
+ const parentDir = path.dirname(skillDir);
871
+ const skillName = path.basename(skillDir);
872
+ await fs.mkdir(parentDir, { recursive: true });
873
+ const stagingDir = await fs.mkdtemp(
874
+ path.join(parentDir, `${skillName}.staging-`)
875
+ );
876
+ const backupDir = path.join(
877
+ parentDir,
878
+ `${skillName}.backup-${Date.now()}-${Math.random().toString(36).slice(2)}`
879
+ );
880
+ let movedExisting = false;
881
+ let promotedStaging = false;
882
+ let cleanupError = null;
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
+ }
897
+ await populateDir(stagingDir);
898
+ if (preserve && await pathExists(skillDir)) {
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.");
909
+ }
910
+ if (await pathExists(skillDir)) {
911
+ await fs.rename(skillDir, backupDir);
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
+ }
919
+ }
920
+ await fs.rename(stagingDir, skillDir);
921
+ promotedStaging = true;
922
+ } catch (error) {
923
+ if (movedExisting && !promotedStaging && await pathExists(backupDir)) {
924
+ await fs.rename(backupDir, skillDir);
925
+ }
926
+ throw error;
927
+ } finally {
928
+ if (!promotedStaging && await pathExists(stagingDir)) {
929
+ await fs.rm(stagingDir, { recursive: true, force: true });
930
+ }
931
+ if ((promotedStaging || !movedExisting) && await pathExists(backupDir)) {
932
+ try {
933
+ await fs.rm(backupDir, { recursive: true, force: true });
934
+ } catch (error) {
935
+ if (!cleanupError) {
936
+ cleanupError = error instanceof Error ? error : new Error(String(error));
937
+ }
938
+ }
939
+ }
940
+ if (cleanupError) {
941
+ console.warn(
942
+ `[Notis] Failed to clean up temporary skill directory backup for "${skillDir}"`,
943
+ cleanupError
944
+ );
945
+ }
946
+ }
947
+ }
948
+ function bundleFilesIncludeSkillMd(bundleFiles) {
949
+ return bundleFiles.some((bundleFile) => {
950
+ const basename = path.basename(bundleFile.path).toLowerCase();
951
+ return basename === "skill.md" || basename === "skills.md";
952
+ });
953
+ }
954
+ async function createSkillBundleBase64(skill) {
955
+ const zipPath = await createZipFromDirectory(skill.directoryPath);
956
+ try {
957
+ const zipBytes = await fs.readFile(zipPath);
958
+ return zipBytes.toString("base64");
959
+ } finally {
960
+ await fs.rm(zipPath, { force: true });
961
+ }
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
+ }
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
+ }
989
+ const skillDir = path.join(
990
+ paths.skillsDir,
991
+ safeName(skill.name, paths.skillsDir)
992
+ );
993
+ const preserve = {
994
+ previouslyApplied: options.previouslyApplied,
995
+ expectedFolderHash: options.expectedFolderHash,
996
+ native: isNativeSkill(skill)
997
+ };
998
+ if (bundleBytes?.length) {
999
+ const bundlePath = path.join(
1000
+ os.tmpdir(),
1001
+ `notis-skill-download-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
1002
+ );
1003
+ try {
1004
+ await fs.writeFile(bundlePath, bundleBytes);
1005
+ let appliedFiles = [];
1006
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
1007
+ appliedFiles = await extractZipToDirectory(bundlePath, stagingDir);
1008
+ }, preserve);
1009
+ return { written: true, appliedFiles };
1010
+ } finally {
1011
+ await fs.rm(bundlePath, { force: true });
1012
+ }
1013
+ }
1014
+ if (!skill.skill_md && (!skill.bundle_files || skill.bundle_files.length === 0)) {
1015
+ return false;
1016
+ }
1017
+ if (skill.bundle_files && skill.bundle_files.length > 0) {
1018
+ if (!bundleFilesIncludeSkillMd(skill.bundle_files)) {
1019
+ throw new Error(
1020
+ `Synced bundle for "${skill.name}" is missing SKILL.md or SKILLS.md`
1021
+ );
1022
+ }
1023
+ const appliedFiles = [];
1024
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
1025
+ for (const bundleFile of skill.bundle_files || []) {
1026
+ const filePath = resolveSkillBundleFilePath(
1027
+ stagingDir,
1028
+ bundleFile.path
1029
+ );
1030
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1031
+ await fs.writeFile(
1032
+ filePath,
1033
+ Buffer.from(bundleFile.content_b64, "base64")
1034
+ );
1035
+ appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
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
+ }
1051
+ }, preserve);
1052
+ return { written: true, appliedFiles: appliedFiles.sort() };
1053
+ }
1054
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
1055
+ await fs.writeFile(
1056
+ path.join(stagingDir, "SKILL.md"),
1057
+ skill.skill_md ?? "",
1058
+ "utf8"
1059
+ );
1060
+ }, preserve);
1061
+ return { written: true, appliedFiles: ["SKILL.md"] };
1062
+ }
1063
+
1064
+ // src/runtime/skill-sync/cloud-client.ts
1065
+ import { createHash as createHash3 } from "node:crypto";
1066
+ async function requestJson(url, jwt, options = {}) {
1067
+ const response = await fetch(url, {
1068
+ signal: AbortSignal.timeout(9e4),
1069
+ method: options.method || "POST",
1070
+ headers: {
1071
+ "Content-Type": "application/json",
1072
+ Authorization: `Bearer ${jwt}`
1073
+ },
1074
+ body: options.body ? JSON.stringify(options.body) : void 0
1075
+ });
1076
+ if (!response.ok) {
1077
+ const text = await response.text();
1078
+ throw new Error(`${options.method || "POST"} ${url} \u2192 ${response.status}: ${text}`);
1079
+ }
1080
+ return response.json();
1081
+ }
1082
+ async function fetchSyncSettings(serverUrl, jwt) {
1083
+ return requestJson(`${serverUrl}/portal_skills/sync-settings`, jwt, {
1084
+ body: {}
1085
+ });
1086
+ }
1087
+ async function pullSkills(serverUrl, jwt) {
1088
+ return requestJson(`${serverUrl}/portal_skills/sync-pull`, jwt, {
1089
+ body: { skill_sync_protocol: 2 }
1090
+ });
1091
+ }
1092
+ async function pushChangedSkills(serverUrl, jwt, changedSkills) {
1093
+ const payloadSkills = await Promise.all(changedSkills.map(async (skill) => ({
1094
+ name: skill.name,
1095
+ description: skill.description,
1096
+ skill_md: skill.skillMd,
1097
+ source_url: skill.sourceUrl,
1098
+ folder_hash: skill.folderHash,
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
+ } : {}
1106
+ })));
1107
+ return requestJson(`${serverUrl}/portal_skills/sync-push`, jwt, {
1108
+ body: {
1109
+ skill_sync_protocol: 2,
1110
+ skills: payloadSkills
1111
+ }
1112
+ });
1113
+ }
1114
+ async function downloadSkillBundle(bundleUrl) {
1115
+ const response = await fetch(bundleUrl, { signal: AbortSignal.timeout(9e4) });
1116
+ if (!response.ok) {
1117
+ const text = await response.text();
1118
+ throw new Error(`GET ${bundleUrl} \u2192 ${response.status}: ${text}`);
1119
+ }
1120
+ return Buffer.from(await response.arrayBuffer());
1121
+ }
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
+ }
1142
+ return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
1143
+ method: "PATCH",
1144
+ body: {
1145
+ skill_id: skillId,
1146
+ agent_targets: targets,
1147
+ ...expectedUpdatedAt ? { expected_updated_at: expectedUpdatedAt } : {}
1148
+ }
1149
+ });
1150
+ }
1151
+
1152
+ // src/runtime/skill-sync/symlink-manager.ts
1153
+ import { promises as fs2 } from "fs";
1154
+ import os2 from "os";
1155
+ import path2 from "path";
1156
+ var HOME_DIR2 = os2.homedir();
1157
+ var EXTERNAL_AGENT_SKILL_DIRS = {
1158
+ claude_code: path2.join(HOME_DIR2, ".claude", "skills"),
1159
+ cursor: path2.join(HOME_DIR2, ".cursor", "skills"),
1160
+ codex: path2.join(HOME_DIR2, ".codex", "skills")
1161
+ };
1162
+ var EXTERNAL_AGENTS = Object.keys(EXTERNAL_AGENT_SKILL_DIRS);
1163
+ var AGENT_FAILURE_LABELS = {
1164
+ claude_code: "Claude Code",
1165
+ cursor: "Cursor",
1166
+ codex: "Codex",
1167
+ legacy: "legacy ~/.agents/skills"
1168
+ };
1169
+ function agentFailureLabel(agent) {
1170
+ return AGENT_FAILURE_LABELS[agent] ?? agent;
1171
+ }
1172
+ function agentFolderFailureName(agent) {
1173
+ return `${agentFailureLabel(agent)} skills folder`;
1174
+ }
1175
+ async function removeForeignAccountSymlinks(skillsDir, options = {}) {
1176
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
1177
+ const currentRoot = path2.resolve(skillsDir);
1178
+ const foreignCapableRoots = [
1179
+ path2.join(path2.resolve(NOTIS_SKILL_SYNC_ROOT), "users"),
1180
+ path2.join(path2.resolve(AGENTS_DIR), "notis", "users"),
1181
+ path2.dirname(path2.dirname(currentRoot))
1182
+ ];
1183
+ let removed = 0;
1184
+ const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
1185
+ for (const agentDir of /* @__PURE__ */ new Set([...Object.values(agentSkillDirs), legacyGlobalSkillsDir])) {
1186
+ let entries;
1187
+ try {
1188
+ entries = await fs2.readdir(agentDir, { withFileTypes: true });
1189
+ } catch (error) {
1190
+ if (error?.code === "ENOENT") continue;
1191
+ throw error;
1192
+ }
1193
+ for (const entry of entries) {
1194
+ if (!entry.isSymbolicLink()) continue;
1195
+ const entryPath = path2.join(agentDir, entry.name);
1196
+ try {
1197
+ const target = await fs2.readlink(entryPath);
1198
+ const resolvedTarget = path2.resolve(path2.dirname(entryPath), target);
1199
+ const belongsToAnyAccount = foreignCapableRoots.some((root) => resolvedTarget.startsWith(`${root}${path2.sep}`));
1200
+ const belongsToCurrentAccount = resolvedTarget === currentRoot || resolvedTarget.startsWith(`${currentRoot}${path2.sep}`);
1201
+ if (belongsToAnyAccount && !belongsToCurrentAccount) {
1202
+ await fs2.unlink(entryPath);
1203
+ removed += 1;
1204
+ }
1205
+ } catch (error) {
1206
+ if (error?.code !== "ENOENT") throw error;
1207
+ }
1208
+ }
1209
+ }
1210
+ return removed;
1211
+ }
1212
+ function managedSkillRoots(skillsDir, legacyGlobalSkillsDir = LEGACY_AGENTS_SKILLS_DIR) {
1213
+ const resolvedSkillsDir = path2.resolve(skillsDir);
1214
+ const scopedUsersRoot = path2.dirname(path2.dirname(resolvedSkillsDir));
1215
+ const roots = [
1216
+ resolvedSkillsDir,
1217
+ path2.resolve(legacyGlobalSkillsDir),
1218
+ path2.join(path2.resolve(NOTIS_SKILL_SYNC_ROOT), "users"),
1219
+ path2.join(path2.resolve(AGENTS_DIR), "notis", "users")
1220
+ ];
1221
+ if (path2.basename(scopedUsersRoot) === "users") {
1222
+ roots.push(scopedUsersRoot);
1223
+ }
1224
+ return roots;
1225
+ }
1226
+ async function isManagedSymlink(linkPath, managedRoots) {
1227
+ try {
1228
+ const stats = await fs2.lstat(linkPath);
1229
+ if (!stats.isSymbolicLink()) {
1230
+ return false;
1231
+ }
1232
+ const target = await fs2.readlink(linkPath);
1233
+ const resolvedTarget = path2.resolve(path2.dirname(linkPath), target);
1234
+ return managedRoots.some((root) => resolvedTarget === root || resolvedTarget.startsWith(`${root}${path2.sep}`));
1235
+ } catch (error) {
1236
+ if (error?.code === "ENOENT") return false;
1237
+ throw error;
1238
+ }
1239
+ }
1240
+ async function ensureCorrectSymlink(linkPath, targetPath) {
1241
+ try {
1242
+ const stats = await fs2.lstat(linkPath);
1243
+ if (stats.isSymbolicLink()) {
1244
+ const currentTarget = await fs2.readlink(linkPath);
1245
+ const resolvedTarget = path2.resolve(path2.dirname(linkPath), currentTarget);
1246
+ if (resolvedTarget === targetPath) {
1247
+ return "skipped";
1248
+ }
1249
+ await fs2.unlink(linkPath);
1250
+ } else {
1251
+ return "blocked";
1252
+ }
1253
+ } catch (error) {
1254
+ if (error?.code !== "ENOENT") throw error;
1255
+ }
1256
+ const relativePath = path2.relative(path2.dirname(linkPath), targetPath);
1257
+ await fs2.symlink(relativePath, linkPath);
1258
+ return "linked";
1259
+ }
1260
+ function defaultAgentSkillDirs(skillsDir) {
1261
+ return {
1262
+ notis: skillsDir,
1263
+ ...EXTERNAL_AGENT_SKILL_DIRS
1264
+ };
1265
+ }
1266
+ async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, failures, agent) {
1267
+ let removed = 0;
1268
+ try {
1269
+ await fs2.mkdir(agentDir, { recursive: true });
1270
+ const existingEntries = await fs2.readdir(agentDir, { withFileTypes: true });
1271
+ for (const entry of existingEntries) {
1272
+ const entryPath = path2.join(agentDir, entry.name);
1273
+ try {
1274
+ if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
1275
+ await fs2.unlink(entryPath);
1276
+ removed += 1;
1277
+ }
1278
+ } catch (error) {
1279
+ if (error?.code !== "ENOENT") {
1280
+ failures.push({ name: entry.name, error: `${agentFailureLabel(agent)}: could not remove skill link (${error.message})` });
1281
+ }
1282
+ }
1283
+ }
1284
+ } catch (error) {
1285
+ failures.push({ name: agentFolderFailureName(agent), error: `Could not read agent skills directory (${error.message})` });
1286
+ }
1287
+ return removed;
1288
+ }
1289
+ async function removeAllSymlinksForSkill(skillName, skillsDir = LEGACY_AGENTS_SKILLS_DIR, options = {}) {
1290
+ let removed = 0;
1291
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
1292
+ const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
1293
+ const managedRoots = managedSkillRoots(skillsDir, legacyGlobalSkillsDir);
1294
+ const agentDirs = /* @__PURE__ */ new Set([...Object.values(agentSkillDirs), legacyGlobalSkillsDir]);
1295
+ const expectedTarget = path2.resolve(skillsDir, safeName(skillName, skillsDir));
1296
+ for (const agentDir of agentDirs) {
1297
+ const linkPath = path2.join(agentDir, safeName(skillName, agentDir));
1298
+ const owned = options.ownership === "exact-skill-dir" ? await isManagedSymlink(linkPath, [expectedTarget]) : await isManagedSymlink(linkPath, managedRoots);
1299
+ if (owned) {
1300
+ await fs2.unlink(linkPath);
1301
+ removed += 1;
1302
+ }
1303
+ }
1304
+ return removed;
1305
+ }
1306
+ async function detectDeletedAgentSymlinks(cloudSkills, previousState, skillsDir = LEGACY_AGENTS_SKILLS_DIR, options = {}) {
1307
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
1308
+ const deletions = [];
1309
+ for (const agent of EXTERNAL_AGENTS) {
1310
+ const agentDir = agentSkillDirs[agent];
1311
+ if (path2.resolve(agentDir) === path2.resolve(skillsDir)) {
1312
+ continue;
1313
+ }
1314
+ try {
1315
+ const dirStat = await fs2.stat(agentDir);
1316
+ if (!dirStat.isDirectory()) {
1317
+ continue;
1318
+ }
1319
+ } catch {
1320
+ continue;
1321
+ }
1322
+ for (const skill of cloudSkills) {
1323
+ if (skill.status !== "active") {
1324
+ continue;
1325
+ }
1326
+ if (!skill.id || skill.id.startsWith("local-")) {
1327
+ continue;
1328
+ }
1329
+ const previous = previousState.skills[skill.name];
1330
+ if (!previous || previous.cloudId !== skill.id || previous.verifiedAgentLinks?.[agent] !== true || !skill.updated_at || previous.cloudUpdatedAt !== skill.updated_at) {
1331
+ continue;
1332
+ }
1333
+ const cloudTargets = normalizeAgentTargets(skill.agent_targets);
1334
+ const previousTargets = normalizeAgentTargets(previous.agentTargets);
1335
+ if (!cloudTargets[agent] || !previousTargets[agent]) {
1336
+ continue;
1337
+ }
1338
+ let safeSkillName;
1339
+ try {
1340
+ safeSkillName = safeName(skill.name, skillsDir);
1341
+ } catch {
1342
+ continue;
1343
+ }
1344
+ const linkPath = path2.join(agentDir, safeName(safeSkillName, agentDir));
1345
+ try {
1346
+ await fs2.lstat(linkPath);
1347
+ } catch (error) {
1348
+ if (error?.code === "ENOENT") {
1349
+ deletions.push({ skillId: skill.id, skillName: skill.name, agent });
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ return deletions;
1355
+ }
1356
+ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, options = {}) {
1357
+ await fs2.mkdir(skillsDir, { recursive: true });
1358
+ const result = {
1359
+ linked: 0,
1360
+ removed: 0,
1361
+ skipped: 0,
1362
+ verifiedAgentLinks: {},
1363
+ failures: []
1364
+ };
1365
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
1366
+ const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
1367
+ const managedRoots = managedSkillRoots(skillsDir, legacyGlobalSkillsDir);
1368
+ const desiredByAgent = {
1369
+ notis: /* @__PURE__ */ new Set(),
1370
+ claude_code: /* @__PURE__ */ new Set(),
1371
+ cursor: /* @__PURE__ */ new Set(),
1372
+ codex: /* @__PURE__ */ new Set()
1373
+ };
1374
+ for (const skill of skills) {
1375
+ if (skill.status !== "active") {
1376
+ continue;
1377
+ }
1378
+ const safeSkillName = safeName(skill.name, skillsDir);
1379
+ const targets = normalizeAgentTargets(skill.agent_targets);
1380
+ if (targets.notis) {
1381
+ desiredByAgent.notis.add(safeSkillName);
1382
+ }
1383
+ if (targets.claude_code) {
1384
+ desiredByAgent.claude_code.add(safeSkillName);
1385
+ }
1386
+ if (targets.cursor) {
1387
+ desiredByAgent.cursor.add(safeSkillName);
1388
+ }
1389
+ if (targets.codex) {
1390
+ desiredByAgent.codex.add(safeSkillName);
1391
+ }
1392
+ }
1393
+ for (const [agent, agentDir] of Object.entries(agentSkillDirs)) {
1394
+ if (path2.resolve(agentDir) === path2.resolve(skillsDir)) {
1395
+ result.skipped += desiredByAgent[agent].size;
1396
+ continue;
1397
+ }
1398
+ try {
1399
+ await fs2.mkdir(agentDir, { recursive: true });
1400
+ } catch (error) {
1401
+ result.failures.push({ name: agentFolderFailureName(agent), error: `Could not create agent skills directory (${error.message})` });
1402
+ continue;
1403
+ }
1404
+ const desiredSkills = desiredByAgent[agent];
1405
+ if (options.removeUndesired !== false) {
1406
+ result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots, result.failures, agent);
1407
+ }
1408
+ for (const skillName of desiredSkills) {
1409
+ const targetPath = path2.join(skillsDir, skillName);
1410
+ const linkPath = path2.join(agentDir, safeName(skillName, agentDir));
1411
+ try {
1412
+ if (!(await fs2.stat(path2.join(targetPath, "SKILL.md"))).isFile()) throw new Error("Missing SKILL.md");
1413
+ } catch {
1414
+ result.skipped += 1;
1415
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: SKILL.md is missing or unreadable` });
1416
+ continue;
1417
+ }
1418
+ let syncOutcome;
1419
+ try {
1420
+ syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
1421
+ } catch (error) {
1422
+ result.skipped += 1;
1423
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: could not create skill link (${error.message})` });
1424
+ continue;
1425
+ }
1426
+ if (syncOutcome === "linked") {
1427
+ result.linked += 1;
1428
+ } else if (syncOutcome === "blocked") {
1429
+ console.warn(
1430
+ `[skill-sync] Could not link "${skillName}" for ${agent}: non-symlink entry blocks ${linkPath}`
1431
+ );
1432
+ result.skipped += 1;
1433
+ result.failures.push({ name: skillName, error: `${agentFailureLabel(agent)}: an existing file or folder blocks the skill link` });
1434
+ } else {
1435
+ result.skipped += 1;
1436
+ }
1437
+ if (syncOutcome !== "blocked") {
1438
+ result.verifiedAgentLinks[skillName] = { ...result.verifiedAgentLinks[skillName], [agent]: true };
1439
+ }
1440
+ }
1441
+ }
1442
+ if (options.removeUndesired !== false && !Object.values(agentSkillDirs).some(
1443
+ (agentDir) => path2.resolve(agentDir) === path2.resolve(legacyGlobalSkillsDir)
1444
+ )) {
1445
+ result.removed += await removeUndesiredManagedSymlinks(
1446
+ legacyGlobalSkillsDir,
1447
+ /* @__PURE__ */ new Set(),
1448
+ managedRoots,
1449
+ result.failures,
1450
+ "legacy"
1451
+ );
1452
+ }
1453
+ return result;
1454
+ }
1455
+
1456
+ // src/runtime/skill-sync/sync-plan.ts
1457
+ function selectCloudSkillsToApply(skills) {
1458
+ const winners = /* @__PURE__ */ new Map();
1459
+ for (const skill of skills) {
1460
+ const current = winners.get(skill.name);
1461
+ if (!current || (skill.updated_at || "") >= (current.updated_at || "")) {
1462
+ winners.set(skill.name, skill);
1463
+ }
1464
+ }
1465
+ return [...winners.values()];
1466
+ }
1467
+ function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudSkills = []) {
1468
+ return localSkills.filter((skill) => {
1469
+ if (cloudCuratedSkillNames.has(skill.name)) {
1470
+ return false;
1471
+ }
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));
1476
+ }
1477
+ if (!previous) {
1478
+ if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
1479
+ return true;
1480
+ }
1481
+ if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
1482
+ return false;
1483
+ }
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 } : {} };
1488
+ });
1489
+ }
1490
+
1491
+ // src/runtime/skill-sync/write-cloud-skill.ts
1492
+ async function writeCloudSkillWithBundleFallback(skill, dependencies) {
1493
+ if (skill.skill_source_url) {
1494
+ try {
1495
+ const bundleBytes = await dependencies.downloadSkillBundle(skill.skill_source_url);
1496
+ const wroteBundleToDisk = await dependencies.writeCloudSkillToDisk(skill, bundleBytes);
1497
+ if (wroteBundleToDisk) {
1498
+ return wroteBundleToDisk;
1499
+ }
1500
+ dependencies.onWarning?.(
1501
+ `Bundle sync for "${skill.name}" produced no local changes, falling back to SKILL.md payload.`,
1502
+ new Error("Bundle write returned false")
1503
+ );
1504
+ } catch (error) {
1505
+ dependencies.onWarning?.(
1506
+ `Failed to apply bundle sync for "${skill.name}", falling back to SKILL.md payload.`,
1507
+ error
1508
+ );
1509
+ }
1510
+ }
1511
+ if (skill.bundle_hydration_failed) {
1512
+ dependencies.onWarning?.(
1513
+ `Skipping markdown fallback for synced skill "${skill.name}" because its stored bundle could not be hydrated by the server.`,
1514
+ new Error("Bundle hydration failed")
1515
+ );
1516
+ return false;
1517
+ }
1518
+ try {
1519
+ return await dependencies.writeCloudSkillToDisk(skill);
1520
+ } catch (error) {
1521
+ dependencies.onWarning?.(
1522
+ `Failed to write synced skill "${skill.name}" to disk.`,
1523
+ error
1524
+ );
1525
+ return false;
1526
+ }
1527
+ }
1528
+
1529
+ // src/runtime/skill-sync/index.ts
1530
+ var BASE_SKILL_NAMES = /* @__PURE__ */ new Set(["notis-apps", "notis-query", "notis-cli"]);
1531
+ function withoutBaseSkills(pullResponse) {
1532
+ return {
1533
+ ...pullResponse,
1534
+ skills: pullResponse.skills.filter((skill) => !BASE_SKILL_NAMES.has(skill.name))
1535
+ };
1536
+ }
1537
+ function withoutBaseSkillState(state) {
1538
+ return {
1539
+ ...state,
1540
+ skills: Object.fromEntries(
1541
+ Object.entries(state.skills).filter(([name]) => !BASE_SKILL_NAMES.has(name))
1542
+ )
1543
+ };
1544
+ }
1545
+ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
1546
+ fetchSyncSettings,
1547
+ pullSkills,
1548
+ pushChangedSkills,
1549
+ downloadSkillBundle,
1550
+ gatherTopLevelLocalSkills,
1551
+ readLegacySyncState,
1552
+ readSyncState,
1553
+ scanLocalSkills,
1554
+ deleteLocalSkill,
1555
+ writeCloudSkillToDisk,
1556
+ writeSyncState,
1557
+ removeAllSymlinksForSkill,
1558
+ syncSymlinks,
1559
+ detectDeletedAgentSymlinks,
1560
+ removeForeignAccountSymlinks,
1561
+ updateAgentTargets
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
+ }
1581
+ function toSkillMap(skills) {
1582
+ return new Map(skills.map((skill) => [skill.name, skill]));
1583
+ }
1584
+ function decodeJwtSubject(jwt) {
1585
+ try {
1586
+ const parts = jwt.split(".");
1587
+ if (parts.length !== 3) return null;
1588
+ const decoded = JSON.parse(
1589
+ Buffer.from(parts[1], "base64url").toString()
1590
+ );
1591
+ return typeof decoded.sub === "string" && decoded.sub.trim() ? decoded.sub.trim() : null;
1592
+ } catch {
1593
+ return null;
1594
+ }
1595
+ }
1596
+ function cloudContentHash(skill) {
1597
+ return createHash4("sha256").update(JSON.stringify({
1598
+ md: skill.skill_md,
1599
+ hash: skill.skill_folder_hash,
1600
+ source: skill.skill_source_url,
1601
+ files: skill.bundle_files?.slice().sort((a, b) => a.path.localeCompare(b.path)),
1602
+ hydrationFailed: skill.bundle_hydration_failed === true
1603
+ })).digest("hex");
1604
+ }
1605
+ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
1606
+ const skillName = cloudSkill.name;
1607
+ const cloudHash = cloudSkill.skill_folder_hash || "";
1608
+ const localSkill = localSkills.get(skillName);
1609
+ if (!localSkill) {
1610
+ return true;
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
+ }
1618
+ const previous = previousState.skills[skillName];
1619
+ if (previous?.folderHash === localSkill.folderHash && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
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) {
1621
+ return false;
1622
+ }
1623
+ if (cloudSkill.source === "curated") {
1624
+ return cloudHash ? cloudHash !== localSkill.folderHash : true;
1625
+ }
1626
+ const localChangedSinceLastSync = !previous || previous.folderHash !== localSkill.folderHash;
1627
+ return !localChangedSinceLastSync && (Boolean(cloudHash) && cloudHash !== localSkill.folderHash || Boolean(previous?.cloudContentHash && previous.cloudContentHash !== cloudContentHash(cloudSkill)));
1628
+ }
1629
+ function collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames) {
1630
+ const applied = {};
1631
+ for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
1632
+ applied[skill.name] = writtenSkillNames.has(skill.name) ? skill.skill_folder_hash || "" : previousState.skills[skill.name]?.appliedCloudFolderHash;
1633
+ }
1634
+ return applied;
1635
+ }
1636
+ function collectAppliedFiles(pullResponse, previousState, appliedFilesByName) {
1637
+ const applied = {};
1638
+ for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
1639
+ applied[skill.name] = appliedFilesByName[skill.name] ?? previousState.skills[skill.name]?.appliedFiles;
1640
+ }
1641
+ return applied;
1642
+ }
1643
+ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}, previousState = { version: 1, lastSyncedAt: null, skills: {} }) {
1644
+ const localSkillMap = toSkillMap(localSkills);
1645
+ const skills = Object.fromEntries(
1646
+ selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
1647
+ const localSkill = localSkillMap.get(skill.name);
1648
+ const appliedCloudFolderHash = appliedRevisions[skill.name];
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
+ }
1660
+ return [
1661
+ skill.name,
1662
+ {
1663
+ cloudId: skill.id,
1664
+ ...nativeState(skill),
1665
+ folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
1666
+ agentTargets: normalizeAgentTargets(skill.agent_targets),
1667
+ verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
1668
+ cloudUpdatedAt: skill.updated_at,
1669
+ ...!failedContentNames.has(skill.name) && !skill.skill_source_url ? { cloudContentHash: cloudContentHash(skill) } : {},
1670
+ ...appliedCloudFolderHash !== void 0 ? { appliedCloudFolderHash } : {},
1671
+ ...appliedFileList !== void 0 ? { appliedFiles: appliedFileList } : {},
1672
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
1673
+ }
1674
+ ];
1675
+ })
1676
+ );
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, {
1691
+ version: 1,
1692
+ lastSyncedAt,
1693
+ skills
1694
+ });
1695
+ }
1696
+ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1697
+ const cloudSkillNames = new Set(
1698
+ pullResponse.skills.map((skill) => skill.name)
1699
+ );
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) => {
1701
+ const previous = previousState.skills[skill.name];
1702
+ return {
1703
+ id: previous?.cloudId || `local-${skill.name}`,
1704
+ name: skill.name,
1705
+ description: skill.description || null,
1706
+ skill_md: skill.skillMd,
1707
+ agent_targets: previous?.agentTargets,
1708
+ skill_folder_hash: skill.folderHash,
1709
+ source: "local",
1710
+ status: "active"
1711
+ };
1712
+ });
1713
+ return [...nativeLinkRows(pullResponse, localSkills), ...localOnlySkills];
1714
+ }
1715
+ function isEmptySyncState(state) {
1716
+ return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
1717
+ }
1718
+ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
1719
+ if (!isEmptySyncState(scopedState) || !legacyState) {
1720
+ return scopedState;
1721
+ }
1722
+ const migratedSkills = Object.fromEntries(
1723
+ localSkills.flatMap((skill) => {
1724
+ const previous = legacyState.skills[skill.name];
1725
+ if (!previous || previous.folderHash !== skill.folderHash) {
1726
+ return [];
1727
+ }
1728
+ return [[skill.name, previous]];
1729
+ })
1730
+ );
1731
+ if (Object.keys(migratedSkills).length === 0) {
1732
+ return scopedState;
1733
+ }
1734
+ return {
1735
+ version: 1,
1736
+ lastSyncedAt: legacyState.lastSyncedAt,
1737
+ skills: migratedSkills
1738
+ };
1739
+ }
1740
+ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}, acknowledgedNative = /* @__PURE__ */ new Map()) {
1741
+ const localSkillMap = toSkillMap(localSkills);
1742
+ const warnSkillSync = (message, error) => {
1743
+ console.warn(`[Notis] ${message}`, error);
1744
+ };
1745
+ let downloaded = 0;
1746
+ for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
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
+ }
1753
+ continue;
1754
+ }
1755
+ const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
1756
+ downloadSkillBundle: deps.downloadSkillBundle,
1757
+ writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
1758
+ // What the last write put there. Everything else in the folder was
1759
+ // produced locally and survives this one.
1760
+ previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
1761
+ ...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}
1762
+ }),
1763
+ onWarning: warnSkillSync
1764
+ });
1765
+ if (outcome) {
1766
+ downloaded += 1;
1767
+ writtenSkillNames.add(cloudSkill.name);
1768
+ if (typeof outcome === "object" && Array.isArray(outcome.appliedFiles)) {
1769
+ appliedFilesByName[cloudSkill.name] = outcome.appliedFiles;
1770
+ }
1771
+ } else {
1772
+ failures.push({ name: cloudSkill.name, error: "Skill content could not be downloaded or written; sync will retry" });
1773
+ }
1774
+ }
1775
+ return downloaded;
1776
+ }
1777
+ function assertSkillsPullAuthorized(pullResponse) {
1778
+ if (!Array.isArray(pullResponse.skills)) throw new Error("Skill inventory is unavailable; existing files were retained.");
1779
+ assertNativePull(pullResponse);
1780
+ if (pullResponse.entitlement_access?.code === "entitlement_upgrade_required" && pullResponse.entitlement_access.entitlement === "skills") {
1781
+ throw new Error(
1782
+ "Skill sync access was denied; preserving existing local skills."
1783
+ );
1784
+ }
1785
+ }
1786
+ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies = {}, options = {}) {
1787
+ const deps = {
1788
+ ...DEFAULT_RUN_SKILL_SYNC_DEPS,
1789
+ ...dependencies
1790
+ };
1791
+ const authUserId = decodeJwtSubject(jwt);
1792
+ if (!authUserId) {
1793
+ throw new Error(
1794
+ "Cannot materialize skills without a valid authenticated desktop session."
1795
+ );
1796
+ }
1797
+ let pullResponse = await deps.pullSkills(serverUrl, jwt);
1798
+ assertSkillsPullAuthorized(pullResponse);
1799
+ if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
1800
+ const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, void 0, pullResponse.sync_namespace);
1801
+ const previousState = await deps.readSyncState(syncPaths);
1802
+ const localSkills = await deps.scanLocalSkills(syncPaths);
1803
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
1804
+ const failedDownloads = [];
1805
+ const writtenSkillNames = /* @__PURE__ */ new Set();
1806
+ const appliedFilesByName = {};
1807
+ const downloaded = await writePulledSkillsToScopedMirror(
1808
+ pullResponse,
1809
+ localSkills,
1810
+ previousState,
1811
+ syncPaths,
1812
+ deps,
1813
+ failedDownloads,
1814
+ writtenSkillNames,
1815
+ appliedFilesByName
1816
+ );
1817
+ const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
1818
+ const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
1819
+ const relinkSkillNames = new Set(options.relinkSkillNames || []);
1820
+ const failures = [...failedDownloads];
1821
+ const verifiedLinks = {};
1822
+ for (const skill of pullResponse.skills) {
1823
+ const previous = previousState.skills[skill.name];
1824
+ if (skill.updated_at && previous?.cloudId === skill.id && previous.cloudUpdatedAt === skill.updated_at) {
1825
+ verifiedLinks[skill.name] = { ...previous.verifiedAgentLinks };
1826
+ }
1827
+ }
1828
+ if (relinkSkillNames.size > 0) {
1829
+ const relinked = await deps.syncSymlinks(
1830
+ nativeLinkRows(pullResponse, finalLocalSkills).filter((skill) => relinkSkillNames.has(skill.name)),
1831
+ syncPaths.skillsDir,
1832
+ { ...isolatedLinkOptions(syncPaths), removeUndesired: false }
1833
+ );
1834
+ failures.push(...(relinked.failures ?? []).filter(
1835
+ (failure) => !failedDownloads.some((download) => download.name === failure.name)
1836
+ ));
1837
+ for (const name of relinkSkillNames) {
1838
+ verifiedLinks[name] = relinked.verifiedAgentLinks?.[name] ?? {};
1839
+ }
1840
+ }
1841
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
1842
+ const materializedState = buildSyncState(
1843
+ pullResponse,
1844
+ finalLocalSkills,
1845
+ lastSyncedAt,
1846
+ verifiedLinks,
1847
+ new Set(failedDownloads.map((item) => item.name)),
1848
+ collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
1849
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
1850
+ previousState
1851
+ );
1852
+ for (const [name, entry] of Object.entries(materializedState.skills)) {
1853
+ if (writtenSkillNames.has(name)) continue;
1854
+ const previous = previousState.skills[name];
1855
+ if (previous) {
1856
+ entry.folderHash = previous.folderHash;
1857
+ entry.cloudContentHash = previous.cloudContentHash;
1858
+ } else {
1859
+ delete materializedState.skills[name];
1860
+ }
1861
+ }
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
+ }
1871
+ await deps.writeSyncState(materializedState, syncPaths);
1872
+ return {
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),
1874
+ pulled: pullResponse.skills.length,
1875
+ downloaded,
1876
+ deleted: 0,
1877
+ removed,
1878
+ lastSyncedAt,
1879
+ ...failures.length ? { failedLinks: failures } : {}
1880
+ };
1881
+ }
1882
+ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures, linkOptions) {
1883
+ if (isEmptySyncState(scopedState)) {
1884
+ return 0;
1885
+ }
1886
+ const deletions = await deps.detectDeletedAgentSymlinks(
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
+ }),
1891
+ previousState,
1892
+ skillsDir,
1893
+ linkOptions
1894
+ );
1895
+ if (deletions.length === 0) {
1896
+ return 0;
1897
+ }
1898
+ const agentsBySkill = /* @__PURE__ */ new Map();
1899
+ for (const deletion of deletions) {
1900
+ const entry = agentsBySkill.get(deletion.skillId) ?? {
1901
+ skillName: deletion.skillName,
1902
+ agents: /* @__PURE__ */ new Set()
1903
+ };
1904
+ entry.agents.add(deletion.agent);
1905
+ agentsBySkill.set(deletion.skillId, entry);
1906
+ }
1907
+ const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1908
+ assertSkillsPullAuthorized(fresh);
1909
+ assertSyncEnvironment(fresh, pullResponse);
1910
+ Object.assign(pullResponse, fresh);
1911
+ let needsRefresh = false;
1912
+ let deactivated = 0;
1913
+ for (const [skillId, { skillName, agents }] of agentsBySkill) {
1914
+ const skill = pullResponse.skills.find((item) => item.id === skillId);
1915
+ const previous = previousState.skills[skillName];
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;
1921
+ const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
1922
+ try {
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)) {
1932
+ throw new Error("Assignment update did not return a verified saved revision");
1933
+ }
1934
+ skill.agent_targets = saved.agent_targets;
1935
+ skill.updated_at = saved.updated_at;
1936
+ if (native) needsRefresh = true;
1937
+ deactivated += agents.size;
1938
+ } catch (error) {
1939
+ needsRefresh = true;
1940
+ failures.push({ name: skillName, error: "Could not save the local agent removal; refreshed saved assignments" });
1941
+ console.warn(`[skill-sync] Assignment changed or could not be saved for "${skillName}"; refreshing before reconciliation.`, error);
1942
+ }
1943
+ }
1944
+ if (needsRefresh) {
1945
+ const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1946
+ assertSkillsPullAuthorized(refreshed);
1947
+ assertSyncEnvironment(refreshed, pullResponse);
1948
+ Object.assign(pullResponse, refreshed);
1949
+ }
1950
+ return deactivated;
1951
+ }
1952
+ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1953
+ const deps = {
1954
+ ...DEFAULT_RUN_SKILL_SYNC_DEPS,
1955
+ ...dependencies
1956
+ };
1957
+ const syncSettings = await deps.fetchSyncSettings(
1958
+ serverUrl,
1959
+ jwt
1960
+ );
1961
+ const syncUserId = syncSettings.user_id?.trim() || decodeJwtSubject(jwt);
1962
+ if (!syncUserId) {
1963
+ throw new Error(
1964
+ "Cannot sync skills without a server-verified account identity."
1965
+ );
1966
+ }
1967
+ const syncPaths = getSkillSyncPathsForUser(syncUserId, void 0, syncSettings.sync_namespace, options.syncHome);
1968
+ if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
1969
+ const foreignLinksRemoved2 = await deps.removeForeignAccountSymlinks(
1970
+ syncPaths.skillsDir,
1971
+ isolatedLinkOptions(syncPaths)
1972
+ );
1973
+ return {
1974
+ syncEnabled: false,
1975
+ pushed: 0,
1976
+ pulled: 0,
1977
+ downloaded: 0,
1978
+ deleted: 0,
1979
+ deactivated: 0,
1980
+ linked: 0,
1981
+ removed: foreignLinksRemoved2,
1982
+ skipped: 0,
1983
+ lastSyncedAt: syncSettings.last_synced_at,
1984
+ failedPushes: []
1985
+ };
1986
+ }
1987
+ let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1988
+ assertSkillsPullAuthorized(pullResponse);
1989
+ assertSyncEnvironment(pullResponse, syncSettings);
1990
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
1991
+ syncPaths.skillsDir,
1992
+ isolatedLinkOptions(syncPaths)
1993
+ );
1994
+ const cloudCuratedSkillNames = new Set(
1995
+ pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
1996
+ );
1997
+ const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
1998
+ const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
1999
+ const assignmentFailures = [];
2000
+ const deactivated = !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
2001
+ serverUrl,
2002
+ jwt,
2003
+ pullResponse,
2004
+ scopedState,
2005
+ scopedState,
2006
+ syncPaths.skillsDir,
2007
+ deps,
2008
+ assignmentFailures,
2009
+ isolatedLinkOptions(syncPaths)
2010
+ ) : 0;
2011
+ assertSyncEnvironment(pullResponse, syncSettings);
2012
+ const authUserId = decodeJwtSubject(jwt);
2013
+ let previousAuthState = null;
2014
+ if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
2015
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId, void 0, void 0, options.syncHome);
2016
+ previousAuthState = await deps.readSyncState(previousAuthPaths);
2017
+ await deps.gatherTopLevelLocalSkills(syncPaths, {
2018
+ sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
2019
+ protectedSkillNames
2020
+ });
2021
+ }
2022
+ if (!syncPaths.isolatedNamespace) {
2023
+ await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
2024
+ }
2025
+ const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
2026
+ const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
2027
+ localSkills,
2028
+ scopedState,
2029
+ !syncPaths.isolatedNamespace && isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
2030
+ ));
2031
+ pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
2032
+ const gatheredSymlinkResult = await deps.syncSymlinks(
2033
+ buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
2034
+ syncPaths.skillsDir,
2035
+ isolatedLinkOptions(syncPaths)
2036
+ );
2037
+ const pushCandidates = getPushCandidates(
2038
+ localSkills,
2039
+ previousState,
2040
+ cloudCuratedSkillNames,
2041
+ new Set(pullResponse.skills.map((skill) => skill.name)),
2042
+ pullResponse.skills
2043
+ );
2044
+ const failedPushes = [];
2045
+ const acknowledgedNative = /* @__PURE__ */ new Map();
2046
+ let writeLocalSkills = localSkills;
2047
+ if (pushCandidates.length > 0) {
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
+ }
2057
+ if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
2058
+ failedPushes.push(...pushResult.failed);
2059
+ console.warn(
2060
+ `[skill-sync] ${pushResult.failed.length} skill(s) were rejected during push: ` + pushResult.failed.map((f) => `${f.name} (${f.error})`).join("; ")
2061
+ );
2062
+ }
2063
+ pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
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);
2068
+ }
2069
+ const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
2070
+ let deleted = 0;
2071
+ for (const skillName of Object.keys(previousState.skills)) {
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
+ }
2078
+ await deps.deleteLocalSkill(skillName, syncPaths);
2079
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
2080
+ deleted += 1;
2081
+ }
2082
+ }
2083
+ const failedDownloads = [];
2084
+ const writtenSkillNames = /* @__PURE__ */ new Set();
2085
+ const appliedFilesByName = {};
2086
+ const downloaded = await writePulledSkillsToScopedMirror(
2087
+ pullResponse,
2088
+ writeLocalSkills,
2089
+ previousState,
2090
+ syncPaths,
2091
+ deps,
2092
+ failedDownloads,
2093
+ writtenSkillNames,
2094
+ appliedFilesByName,
2095
+ acknowledgedNative
2096
+ );
2097
+ const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
2098
+ const symlinkResult = await deps.syncSymlinks(
2099
+ buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
2100
+ syncPaths.skillsDir,
2101
+ isolatedLinkOptions(syncPaths)
2102
+ );
2103
+ const verifiedLinks = { ...symlinkResult.verifiedAgentLinks ?? {} };
2104
+ for (const failure of failedDownloads) delete verifiedLinks[failure.name];
2105
+ const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
2106
+ await deps.writeSyncState(
2107
+ buildSyncState(
2108
+ pullResponse,
2109
+ finalLocalSkills,
2110
+ lastSyncedAt,
2111
+ verifiedLinks,
2112
+ new Set(failedDownloads.map((item) => item.name)),
2113
+ collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
2114
+ collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
2115
+ previousState
2116
+ ),
2117
+ syncPaths
2118
+ );
2119
+ return {
2120
+ syncEnabled: true,
2121
+ pushed: pushCandidates.length,
2122
+ pulled: pullResponse.skills.length,
2123
+ downloaded,
2124
+ deleted,
2125
+ deactivated,
2126
+ linked: gatheredSymlinkResult.linked + symlinkResult.linked,
2127
+ removed: foreignLinksRemoved + gatheredSymlinkResult.removed + symlinkResult.removed,
2128
+ skipped: symlinkResult.skipped,
2129
+ failedLinks: [...assignmentFailures, ...failedDownloads, ...(symlinkResult.failures ?? []).filter(
2130
+ (failure) => !failedDownloads.some((download) => download.name === failure.name)
2131
+ )],
2132
+ lastSyncedAt,
2133
+ failedPushes
2134
+ };
2135
+ }
2136
+ export {
2137
+ fetchSyncSettings,
2138
+ isolatedLinkOptions,
2139
+ materializeCloudSkillsForLocalShell,
2140
+ runSkillSync,
2141
+ shouldWriteCloudSkill
2142
+ };
2143
+ //# sourceMappingURL=index.js.map