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