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