@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/skill-sync-worker.js
|
|
2
2
|
import { mkdirSync as mkdirSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3
3
|
import { homedir as homedir4 } from "node:os";
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join6 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/runtime/profiles.js
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
@@ -1256,13 +1256,16 @@ async function refreshOAuthCredential(runtime, fetchImpl = fetch) {
|
|
|
1256
1256
|
}
|
|
1257
1257
|
|
|
1258
1258
|
// dist/skill-sync/index.js
|
|
1259
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1260
|
+
import { join as join3 } from "node:path";
|
|
1259
1261
|
import { createHash as createHash2 } from "node:crypto";
|
|
1260
|
-
import { createHash as
|
|
1262
|
+
import { createHash as createHash22 } from "crypto";
|
|
1261
1263
|
import { execFile } from "child_process";
|
|
1262
1264
|
import { promises as fs } from "fs";
|
|
1263
1265
|
import os from "os";
|
|
1264
1266
|
import path from "path";
|
|
1265
1267
|
import { promisify } from "util";
|
|
1268
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1266
1269
|
import { promises as fs2 } from "fs";
|
|
1267
1270
|
import os2 from "os";
|
|
1268
1271
|
import path2 from "path";
|
|
@@ -1280,8 +1283,122 @@ function normalizeAgentTargets(targets) {
|
|
|
1280
1283
|
codex: Boolean(targets?.codex ?? DEFAULT_AGENT_TARGETS.codex)
|
|
1281
1284
|
};
|
|
1282
1285
|
}
|
|
1286
|
+
var NATIVE_IDENTITY_FILE = ".notis-native-skill.json";
|
|
1287
|
+
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
1288
|
+
function checkedNativeReference(value, cloudId) {
|
|
1289
|
+
const reference = value;
|
|
1290
|
+
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)) {
|
|
1291
|
+
throw new Error("Native Skill identity is unavailable. Refresh before syncing.");
|
|
1292
|
+
}
|
|
1293
|
+
return structuredClone(reference);
|
|
1294
|
+
}
|
|
1295
|
+
function nativeDirectory(reference) {
|
|
1296
|
+
return "native-" + checkedNativeReference(reference).skill_id;
|
|
1297
|
+
}
|
|
1298
|
+
function sameNativeReference(left, right) {
|
|
1299
|
+
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);
|
|
1300
|
+
}
|
|
1301
|
+
function isNativeSkill(skill) {
|
|
1302
|
+
return skill.source === "native" || skill.native_reference !== void 0;
|
|
1303
|
+
}
|
|
1304
|
+
function assertNativePull(response) {
|
|
1305
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1306
|
+
for (const skill of response.skills) {
|
|
1307
|
+
if (!isNativeSkill(skill)) continue;
|
|
1308
|
+
const reference = checkedNativeReference(skill.native_reference, skill.id);
|
|
1309
|
+
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") {
|
|
1310
|
+
throw new Error("Native Skill inventory is incomplete. Existing local files were retained.");
|
|
1311
|
+
}
|
|
1312
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1313
|
+
let entrypoint = false;
|
|
1314
|
+
for (const file of skill.bundle_files) {
|
|
1315
|
+
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) {
|
|
1316
|
+
throw new Error("Native Skill bundle has a conflicting managed path.");
|
|
1317
|
+
}
|
|
1318
|
+
paths.add(file.path.toLowerCase());
|
|
1319
|
+
if (file.path === "SKILL.md") {
|
|
1320
|
+
entrypoint = Buffer.from(file.content_b64, "base64").equals(Buffer.from(skill.skill_md, "utf8"));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (!entrypoint) throw new Error("Native Skill instructions and files do not match.");
|
|
1324
|
+
ids.add(skill.id);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
function nativeFileDigest(bytes) {
|
|
1328
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
1329
|
+
}
|
|
1330
|
+
function nativeFrontmatterName(markdown) {
|
|
1331
|
+
const header = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
1332
|
+
return header?.[1].match(/^name\s*:\s*(.+)$/m)?.[1].trim().replace(/^['"]|['"]$/g, "") || "";
|
|
1333
|
+
}
|
|
1334
|
+
function checkedNativeIdentity(value) {
|
|
1335
|
+
const identity = value;
|
|
1336
|
+
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))) {
|
|
1337
|
+
throw new Error("Native Skill provenance is incomplete. Existing files were retained.");
|
|
1338
|
+
}
|
|
1339
|
+
return { ...identity, reference: checkedNativeReference(identity.reference, identity.cloudId) };
|
|
1340
|
+
}
|
|
1341
|
+
function nativeState(skill) {
|
|
1342
|
+
if (!isNativeSkill(skill)) return {};
|
|
1343
|
+
return {
|
|
1344
|
+
nativeReference: checkedNativeReference(skill.native_reference, skill.id),
|
|
1345
|
+
...skill.native_settings ? { nativeSettingsRevision: skill.native_settings.revision } : {},
|
|
1346
|
+
nativeDisplayName: skill.display_name || skill.name,
|
|
1347
|
+
nativeScope: skill.native_scope,
|
|
1348
|
+
nativeUnavailable: false
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function bindNativeFolders(response, locals, state) {
|
|
1352
|
+
const nativeIds = new Set(response.skills.filter(isNativeSkill).map((skill) => skill.id));
|
|
1353
|
+
for (const entry of Object.values(state.skills)) if (entry.nativeReference || entry.nativeUnavailable) nativeIds.add(entry.cloudId);
|
|
1354
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1355
|
+
for (const local of locals) {
|
|
1356
|
+
const id = local.cloudId || state.skills[local.name]?.cloudId;
|
|
1357
|
+
if (!id || !nativeIds.has(id) && !local.nativeReference) continue;
|
|
1358
|
+
if (local.nativeReference) checkedNativeReference(local.nativeReference, id);
|
|
1359
|
+
if (byId.has(id)) throw new Error("Two local folders claim the same native Skill. Keep both copies and resolve the conflict.");
|
|
1360
|
+
byId.set(id, local);
|
|
1361
|
+
}
|
|
1362
|
+
const skills = response.skills.map((skill) => isNativeSkill(skill) && byId.has(skill.id) ? { ...skill, name: byId.get(skill.id).name } : skill);
|
|
1363
|
+
for (const skill of skills.filter(isNativeSkill)) {
|
|
1364
|
+
if (skills.some((other) => other.id !== skill.id && other.name === skill.name)) {
|
|
1365
|
+
throw new Error("A native Skill and another Skill share a local folder. Existing files were retained.");
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return { ...response, skills };
|
|
1369
|
+
}
|
|
1370
|
+
function previousNativeState(state, skill) {
|
|
1371
|
+
const named = state.skills[skill.name];
|
|
1372
|
+
return named?.cloudId === skill.id ? named : Object.values(state.skills).find((entry) => entry.cloudId === skill.id);
|
|
1373
|
+
}
|
|
1374
|
+
function nativeLocalReady(skill, local) {
|
|
1375
|
+
return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash && local.cloudId === skill.id && local.nativeScope === skill.native_scope && sameNativeReference(local.nativeReference, skill.native_reference));
|
|
1376
|
+
}
|
|
1377
|
+
function nativeLinkRows(response, locals) {
|
|
1378
|
+
return response.skills.map((skill) => isNativeSkill(skill) && !nativeLocalReady(skill, locals.find((row) => row.name === skill.name)) ? { ...skill, status: "disabled" } : skill);
|
|
1379
|
+
}
|
|
1380
|
+
function retainMissingNativeState(previous, next) {
|
|
1381
|
+
const present = new Set(Object.values(next.skills).map((row) => row.cloudId));
|
|
1382
|
+
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: {} }]));
|
|
1383
|
+
const history = structuredClone(previous.nativeHistory || {});
|
|
1384
|
+
for (const [name, row] of [...Object.entries(previous.skills), ...Object.entries(next.skills)]) {
|
|
1385
|
+
if (!row.nativeReference && !row.nativeUnavailable) continue;
|
|
1386
|
+
const names = history[row.cloudId]?.names || [];
|
|
1387
|
+
history[row.cloudId] = { names: [.../* @__PURE__ */ new Set([...names, name])] };
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
...next,
|
|
1391
|
+
skills: { ...retained, ...next.skills },
|
|
1392
|
+
...Object.keys(history).length ? { nativeHistory: history } : {}
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function hasNativeFolderHistory(state, name) {
|
|
1396
|
+
return Object.values(state.nativeHistory || {}).some((row) => row.names.includes(name));
|
|
1397
|
+
}
|
|
1283
1398
|
var HOME_DIR = os.homedir();
|
|
1284
1399
|
var execFileAsync = promisify(execFile);
|
|
1400
|
+
var NativeSkillIdentityError = class extends Error {
|
|
1401
|
+
};
|
|
1285
1402
|
var AGENTS_DIR = path.join(HOME_DIR, ".agents");
|
|
1286
1403
|
var LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
1287
1404
|
var NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
|
|
@@ -1341,15 +1458,18 @@ function sanitizePathSegment(value) {
|
|
|
1341
1458
|
if (sanitized === raw) {
|
|
1342
1459
|
return sanitized;
|
|
1343
1460
|
}
|
|
1344
|
-
return `${sanitized}-${
|
|
1461
|
+
return `${sanitized}-${createHash22("sha256").update(raw).digest("hex").slice(0, 12)}`;
|
|
1345
1462
|
}
|
|
1346
|
-
function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR) {
|
|
1463
|
+
function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR, namespace, syncHome) {
|
|
1347
1464
|
const safeUserId = sanitizePathSegment(authUserId);
|
|
1348
|
-
const
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1465
|
+
const homeDir = syncHome ? path.resolve(syncHome) : void 0;
|
|
1466
|
+
const resolvedAgentsDir = homeDir ? path.join(homeDir, ".agents") : path.resolve(agentsDir);
|
|
1467
|
+
const syncRoot = homeDir ? path.join(homeDir, ".notis", "skills") : getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
|
|
1468
|
+
const userRoot = namespace ? path.join(syncRoot, "environments", sanitizePathSegment(namespace), "users", safeUserId) : path.join(syncRoot, "users", safeUserId);
|
|
1351
1469
|
const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
|
|
1352
1470
|
return {
|
|
1471
|
+
...homeDir ? { homeDir } : {},
|
|
1472
|
+
...namespace ? { isolatedNamespace: sanitizePathSegment(namespace) } : {},
|
|
1353
1473
|
agentsDir: resolvedAgentsDir,
|
|
1354
1474
|
syncRoot,
|
|
1355
1475
|
legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
|
|
@@ -1404,16 +1524,17 @@ function isIgnoredSkillEntry(entry) {
|
|
|
1404
1524
|
if (entry.isDirectory()) return IGNORED_SKILL_DIRECTORIES.has(entry.name);
|
|
1405
1525
|
return /\.py[co]$/i.test(entry.name);
|
|
1406
1526
|
}
|
|
1407
|
-
async function listFilesRecursive(dirPath) {
|
|
1527
|
+
async function listFilesRecursive(dirPath, rootPath = dirPath, strictLinks = false) {
|
|
1408
1528
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
1409
1529
|
const nested = await Promise.all(
|
|
1410
1530
|
entries.map(async (entry) => {
|
|
1411
|
-
if (isIgnoredSkillEntry(entry)) {
|
|
1531
|
+
if (isIgnoredSkillEntry(entry) || dirPath === rootPath && entry.name === NATIVE_IDENTITY_FILE) {
|
|
1412
1532
|
return [];
|
|
1413
1533
|
}
|
|
1414
1534
|
const fullPath = path.join(dirPath, entry.name);
|
|
1535
|
+
if (strictLinks && entry.isSymbolicLink()) throw new Error("Native Skill bundles cannot contain symbolic links.");
|
|
1415
1536
|
if (entry.isDirectory()) {
|
|
1416
|
-
return listFilesRecursive(fullPath);
|
|
1537
|
+
return listFilesRecursive(fullPath, rootPath, strictLinks);
|
|
1417
1538
|
}
|
|
1418
1539
|
if (entry.isFile()) {
|
|
1419
1540
|
return [fullPath];
|
|
@@ -1424,7 +1545,7 @@ async function listFilesRecursive(dirPath) {
|
|
|
1424
1545
|
return nested.flat().sort();
|
|
1425
1546
|
}
|
|
1426
1547
|
async function computeFolderHash(dirPath) {
|
|
1427
|
-
const hash =
|
|
1548
|
+
const hash = createHash22("sha256");
|
|
1428
1549
|
const filePaths = await listFilesRecursive(dirPath);
|
|
1429
1550
|
for (const filePath of filePaths) {
|
|
1430
1551
|
const relativePath = path.relative(dirPath, filePath);
|
|
@@ -1502,17 +1623,17 @@ function defaultTopLevelSkillSources(paths) {
|
|
|
1502
1623
|
{ label: "agents", root: paths.legacySkillsDir, priority: 2 },
|
|
1503
1624
|
{
|
|
1504
1625
|
label: "codex",
|
|
1505
|
-
root: path.join(HOME_DIR, ".codex", "skills"),
|
|
1626
|
+
root: path.join(paths.homeDir || HOME_DIR, ".codex", "skills"),
|
|
1506
1627
|
priority: 3
|
|
1507
1628
|
},
|
|
1508
1629
|
{
|
|
1509
1630
|
label: "cursor",
|
|
1510
|
-
root: path.join(HOME_DIR, ".cursor", "skills"),
|
|
1631
|
+
root: path.join(paths.homeDir || HOME_DIR, ".cursor", "skills"),
|
|
1511
1632
|
priority: 4
|
|
1512
1633
|
},
|
|
1513
1634
|
{
|
|
1514
1635
|
label: "claude",
|
|
1515
|
-
root: path.join(HOME_DIR, ".claude", "skills"),
|
|
1636
|
+
root: path.join(paths.homeDir || HOME_DIR, ".claude", "skills"),
|
|
1516
1637
|
priority: 5
|
|
1517
1638
|
}
|
|
1518
1639
|
);
|
|
@@ -1732,6 +1853,7 @@ async function ensureCanonicalSkillsDir(paths = DEFAULT_SYNC_PATHS) {
|
|
|
1732
1853
|
async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
|
|
1733
1854
|
await ensureCanonicalSkillsDir(paths);
|
|
1734
1855
|
const sourceUrls = await readSkillLock(paths);
|
|
1856
|
+
const previousState = await readSyncState(paths);
|
|
1735
1857
|
const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
|
|
1736
1858
|
const skills = await Promise.all(
|
|
1737
1859
|
entries.map(async (entry) => {
|
|
@@ -1740,6 +1862,15 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
|
|
|
1740
1862
|
}
|
|
1741
1863
|
const skillDir = path.join(paths.skillsDir, entry.name);
|
|
1742
1864
|
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
1865
|
+
let marker = null;
|
|
1866
|
+
try {
|
|
1867
|
+
marker = await fs.readFile(path.join(skillDir, NATIVE_IDENTITY_FILE), "utf8");
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
if (error.code !== "ENOENT") {
|
|
1870
|
+
throw new NativeSkillIdentityError("Native Skill provenance could not be read; existing files were retained.");
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
const knownNative = marker !== null || Boolean(previousState.skills[entry.name]?.nativeReference) || Object.values(previousState.nativeHistory || {}).some((row) => row.names.includes(entry.name));
|
|
1743
1874
|
try {
|
|
1744
1875
|
const skillMd = await fs.readFile(skillMdPath, "utf8");
|
|
1745
1876
|
const { description } = parseFrontMatter(skillMd);
|
|
@@ -1751,11 +1882,38 @@ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
|
|
|
1751
1882
|
folderHash,
|
|
1752
1883
|
directoryPath: skillDir
|
|
1753
1884
|
};
|
|
1885
|
+
if (marker !== null) {
|
|
1886
|
+
let identity;
|
|
1887
|
+
try {
|
|
1888
|
+
identity = checkedNativeIdentity(JSON.parse(marker));
|
|
1889
|
+
} catch {
|
|
1890
|
+
throw new NativeSkillIdentityError("Native Skill provenance is unreadable; do not upload it as a new Skill.");
|
|
1891
|
+
}
|
|
1892
|
+
let current = true;
|
|
1893
|
+
for (const [name, expected] of Object.entries(identity.files)) {
|
|
1894
|
+
try {
|
|
1895
|
+
if (nativeFileDigest(await fs.readFile(resolveSkillBundleFilePath(skillDir, name))) !== expected) current = false;
|
|
1896
|
+
} catch {
|
|
1897
|
+
current = false;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
const editedName = nativeFrontmatterName(skillMd);
|
|
1901
|
+
Object.assign(skill, {
|
|
1902
|
+
cloudId: identity.cloudId,
|
|
1903
|
+
nativeReference: identity.reference,
|
|
1904
|
+
nativeScope: identity.scope,
|
|
1905
|
+
nativeBaseFolderHash: identity.folderHash,
|
|
1906
|
+
nativeFilesCurrent: current,
|
|
1907
|
+
displayName: editedName && editedName !== identity.frontmatterName ? editedName : identity.displayName
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1754
1910
|
if (sourceUrls[entry.name]) {
|
|
1755
1911
|
skill.sourceUrl = sourceUrls[entry.name];
|
|
1756
1912
|
}
|
|
1757
1913
|
return skill;
|
|
1758
|
-
} catch {
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
if (error instanceof NativeSkillIdentityError) throw error;
|
|
1916
|
+
if (knownNative) throw new NativeSkillIdentityError("Native Skill files could not be read; existing files were retained.");
|
|
1759
1917
|
return null;
|
|
1760
1918
|
}
|
|
1761
1919
|
})
|
|
@@ -1766,6 +1924,9 @@ function normalizeSyncState(state) {
|
|
|
1766
1924
|
if (!state || state.version !== 1 || typeof state.skills !== "object") {
|
|
1767
1925
|
return null;
|
|
1768
1926
|
}
|
|
1927
|
+
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")))) {
|
|
1928
|
+
throw new Error("Native Skill sync provenance is unreadable; existing files were retained.");
|
|
1929
|
+
}
|
|
1769
1930
|
const normalizeStoredAgentTargets = (targets) => ({
|
|
1770
1931
|
notis: Boolean(targets?.notis ?? true),
|
|
1771
1932
|
claude_code: Boolean(targets?.claude_code ?? true),
|
|
@@ -1774,6 +1935,7 @@ function normalizeSyncState(state) {
|
|
|
1774
1935
|
});
|
|
1775
1936
|
return {
|
|
1776
1937
|
version: 1,
|
|
1938
|
+
...state.nativeHistory ? { nativeHistory: state.nativeHistory } : {},
|
|
1777
1939
|
lastSyncedAt: typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
|
|
1778
1940
|
skills: Object.fromEntries(
|
|
1779
1941
|
Object.entries(state.skills || {}).map(([skillName, skillState]) => [
|
|
@@ -1933,13 +2095,18 @@ async function listRelativeFiles(dirPath) {
|
|
|
1933
2095
|
function toPosixRelativePath(rootDir, filePath) {
|
|
1934
2096
|
return path.relative(rootDir, filePath).split(path.sep).join("/");
|
|
1935
2097
|
}
|
|
1936
|
-
async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied) {
|
|
2098
|
+
async function preserveUnmanagedFiles(existingDir, stagingDir, previouslyApplied, strictConflicts = false) {
|
|
1937
2099
|
const managed = previouslyApplied ? new Set(previouslyApplied) : null;
|
|
1938
2100
|
for (const existingFile of await listFilesRecursive(existingDir)) {
|
|
1939
2101
|
const relativePath = toPosixRelativePath(existingDir, existingFile);
|
|
1940
2102
|
if (managed?.has(relativePath)) continue;
|
|
1941
2103
|
const stagedPath = path.join(stagingDir, ...relativePath.split("/"));
|
|
1942
|
-
if (await pathExists(stagedPath))
|
|
2104
|
+
if (await pathExists(stagedPath)) {
|
|
2105
|
+
if (strictConflicts && !(await fs.readFile(existingFile)).equals(await fs.readFile(stagedPath))) {
|
|
2106
|
+
throw new Error("An incoming native Skill file conflicts with a local file. Local files were retained.");
|
|
2107
|
+
}
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
1943
2110
|
await fs.mkdir(path.dirname(stagedPath), { recursive: true });
|
|
1944
2111
|
await fs.copyFile(existingFile, stagedPath);
|
|
1945
2112
|
}
|
|
@@ -1967,13 +2134,41 @@ async function replaceSkillDirectoryAtomically(skillDir, populateDir, preserve)
|
|
|
1967
2134
|
let promotedStaging = false;
|
|
1968
2135
|
let cleanupError = null;
|
|
1969
2136
|
try {
|
|
2137
|
+
if (preserve?.native && preserve.expectedFolderHash === void 0) {
|
|
2138
|
+
let existing = false;
|
|
2139
|
+
try {
|
|
2140
|
+
await fs.lstat(skillDir);
|
|
2141
|
+
existing = true;
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
if (error.code !== "ENOENT") throw error;
|
|
2144
|
+
}
|
|
2145
|
+
if (existing) throw new Error("Existing native Skill files need a readable baseline before replacement.");
|
|
2146
|
+
}
|
|
2147
|
+
if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
2148
|
+
throw new Error("Local Skill files changed before sync; the newer local edits were retained.");
|
|
2149
|
+
}
|
|
1970
2150
|
await populateDir(stagingDir);
|
|
1971
2151
|
if (preserve && await pathExists(skillDir)) {
|
|
1972
|
-
await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied);
|
|
2152
|
+
await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied, preserve.native);
|
|
2153
|
+
}
|
|
2154
|
+
if (preserve?.native) {
|
|
2155
|
+
const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
|
|
2156
|
+
const marker = checkedNativeIdentity(JSON.parse(await fs.readFile(markerPath, "utf8")));
|
|
2157
|
+
marker.folderHash = await computeFolderHash(stagingDir);
|
|
2158
|
+
await fs.writeFile(markerPath, JSON.stringify(marker), { mode: 384 });
|
|
2159
|
+
}
|
|
2160
|
+
if (preserve?.expectedFolderHash !== void 0 && (!await pathExists(skillDir) || await computeFolderHash(skillDir) !== preserve.expectedFolderHash)) {
|
|
2161
|
+
throw new Error("Local Skill files changed during sync; the newer local edits were retained.");
|
|
1973
2162
|
}
|
|
1974
2163
|
if (await pathExists(skillDir)) {
|
|
1975
2164
|
await fs.rename(skillDir, backupDir);
|
|
1976
2165
|
movedExisting = true;
|
|
2166
|
+
if (preserve?.native && preserve.expectedFolderHash === void 0) {
|
|
2167
|
+
throw new Error("Native Skill files appeared during sync; existing files were retained.");
|
|
2168
|
+
}
|
|
2169
|
+
if (preserve?.expectedFolderHash !== void 0 && await computeFolderHash(backupDir) !== preserve.expectedFolderHash) {
|
|
2170
|
+
throw new Error("Local Skill files changed before publication; the newer local edits were retained.");
|
|
2171
|
+
}
|
|
1977
2172
|
}
|
|
1978
2173
|
await fs.rename(stagingDir, skillDir);
|
|
1979
2174
|
promotedStaging = true;
|
|
@@ -2018,12 +2213,41 @@ async function createSkillBundleBase64(skill) {
|
|
|
2018
2213
|
await fs.rm(zipPath, { force: true });
|
|
2019
2214
|
}
|
|
2020
2215
|
}
|
|
2216
|
+
async function captureNativeSkillFiles(skill) {
|
|
2217
|
+
const hash = createHash22("sha256");
|
|
2218
|
+
const files = [];
|
|
2219
|
+
for (const filePath of await listFilesRecursive(skill.directoryPath, skill.directoryPath, true)) {
|
|
2220
|
+
const relative2 = path.relative(skill.directoryPath, filePath);
|
|
2221
|
+
const bytes = await fs.readFile(filePath);
|
|
2222
|
+
hash.update(relative2);
|
|
2223
|
+
hash.update("\0");
|
|
2224
|
+
hash.update(bytes);
|
|
2225
|
+
hash.update("\0");
|
|
2226
|
+
files.push({ path: relative2.split(path.sep).join("/"), content_b64: bytes.toString("base64") });
|
|
2227
|
+
}
|
|
2228
|
+
if (hash.digest("hex") !== skill.folderHash) throw new Error("Local Skill files changed before upload. Sync again with the current files.");
|
|
2229
|
+
return files;
|
|
2230
|
+
}
|
|
2021
2231
|
async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PATHS, options = {}) {
|
|
2232
|
+
if (isNativeSkill(skill)) {
|
|
2233
|
+
assertNativePull({
|
|
2234
|
+
skills: [{ ...skill, name: "native-" + skill.id }],
|
|
2235
|
+
skill_sync_protocol: 2,
|
|
2236
|
+
sync_scope: skill.native_scope,
|
|
2237
|
+
sync_enabled: true,
|
|
2238
|
+
last_synced_at: null
|
|
2239
|
+
});
|
|
2240
|
+
if (bundleBytes?.length) throw new Error("Native Skills require their verified complete file response.");
|
|
2241
|
+
}
|
|
2022
2242
|
const skillDir = path.join(
|
|
2023
2243
|
paths.skillsDir,
|
|
2024
2244
|
safeName(skill.name, paths.skillsDir)
|
|
2025
2245
|
);
|
|
2026
|
-
const preserve = {
|
|
2246
|
+
const preserve = {
|
|
2247
|
+
previouslyApplied: options.previouslyApplied,
|
|
2248
|
+
expectedFolderHash: options.expectedFolderHash,
|
|
2249
|
+
native: isNativeSkill(skill)
|
|
2250
|
+
};
|
|
2027
2251
|
if (bundleBytes?.length) {
|
|
2028
2252
|
const bundlePath = path.join(
|
|
2029
2253
|
os.tmpdir(),
|
|
@@ -2063,6 +2287,20 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
|
|
|
2063
2287
|
);
|
|
2064
2288
|
appliedFiles.push(toPosixRelativePath(stagingDir, filePath));
|
|
2065
2289
|
}
|
|
2290
|
+
if (skill.native_reference) {
|
|
2291
|
+
const identity = {
|
|
2292
|
+
schema: 1,
|
|
2293
|
+
cloudId: skill.id,
|
|
2294
|
+
reference: skill.native_reference,
|
|
2295
|
+
displayName: skill.display_name || skill.name,
|
|
2296
|
+
frontmatterName: nativeFrontmatterName(skill.skill_md || ""),
|
|
2297
|
+
scope: skill.native_scope,
|
|
2298
|
+
folderHash: await computeFolderHash(stagingDir),
|
|
2299
|
+
files: Object.fromEntries((skill.bundle_files || []).map((file) => [file.path, nativeFileDigest(Buffer.from(file.content_b64, "base64"))]))
|
|
2300
|
+
};
|
|
2301
|
+
await fs.writeFile(path.join(stagingDir, NATIVE_IDENTITY_FILE), JSON.stringify(identity), { mode: 384 });
|
|
2302
|
+
appliedFiles.push(NATIVE_IDENTITY_FILE);
|
|
2303
|
+
}
|
|
2066
2304
|
}, preserve);
|
|
2067
2305
|
return { written: true, appliedFiles: appliedFiles.sort() };
|
|
2068
2306
|
}
|
|
@@ -2098,7 +2336,7 @@ async function fetchSyncSettings(serverUrl, jwt) {
|
|
|
2098
2336
|
}
|
|
2099
2337
|
async function pullSkills(serverUrl, jwt) {
|
|
2100
2338
|
return requestJson(`${serverUrl}/portal_skills/sync-pull`, jwt, {
|
|
2101
|
-
body: {}
|
|
2339
|
+
body: { skill_sync_protocol: 2 }
|
|
2102
2340
|
});
|
|
2103
2341
|
}
|
|
2104
2342
|
async function pushChangedSkills(serverUrl, jwt, changedSkills) {
|
|
@@ -2108,10 +2346,17 @@ async function pushChangedSkills(serverUrl, jwt, changedSkills) {
|
|
|
2108
2346
|
skill_md: skill.skillMd,
|
|
2109
2347
|
source_url: skill.sourceUrl,
|
|
2110
2348
|
folder_hash: skill.folderHash,
|
|
2111
|
-
bundle_base64: await createSkillBundleBase64(skill)
|
|
2349
|
+
...skill.nativeReference ? { bundle_files: await captureNativeSkillFiles(skill) } : { bundle_base64: await createSkillBundleBase64(skill) },
|
|
2350
|
+
...skill.cloudId ? { cloud_id: skill.cloudId } : {},
|
|
2351
|
+
...skill.nativeReference ? {
|
|
2352
|
+
native_reference: skill.nativeReference,
|
|
2353
|
+
display_name: skill.displayName,
|
|
2354
|
+
native_scope: skill.nativeScope
|
|
2355
|
+
} : {}
|
|
2112
2356
|
})));
|
|
2113
2357
|
return requestJson(`${serverUrl}/portal_skills/sync-push`, jwt, {
|
|
2114
2358
|
body: {
|
|
2359
|
+
skill_sync_protocol: 2,
|
|
2115
2360
|
skills: payloadSkills
|
|
2116
2361
|
}
|
|
2117
2362
|
});
|
|
@@ -2124,7 +2369,26 @@ async function downloadSkillBundle(bundleUrl) {
|
|
|
2124
2369
|
}
|
|
2125
2370
|
return Buffer.from(await response.arrayBuffer());
|
|
2126
2371
|
}
|
|
2127
|
-
async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt) {
|
|
2372
|
+
async function updateAgentTargets(serverUrl, jwt, skillId, targets, expectedUpdatedAt, native) {
|
|
2373
|
+
if (native) {
|
|
2374
|
+
checkedNativeReference(native.reference, skillId);
|
|
2375
|
+
if (!Number.isSafeInteger(native.settingsRevision) || native.settingsRevision < 0) throw new Error("Refresh native Skill settings before changing assignments.");
|
|
2376
|
+
const payload = {
|
|
2377
|
+
operation: "update",
|
|
2378
|
+
target: { skill_id: skillId, access_revision: native.reference.access_revision },
|
|
2379
|
+
revision: native.settingsRevision,
|
|
2380
|
+
patch: { agent_targets: Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b))) }
|
|
2381
|
+
};
|
|
2382
|
+
const requestId = "sync-settings-" + createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
2383
|
+
const saved = await requestJson(
|
|
2384
|
+
`${serverUrl}/portal_skills/native-settings`,
|
|
2385
|
+
jwt,
|
|
2386
|
+
{ body: { ...payload, request_id: requestId } }
|
|
2387
|
+
);
|
|
2388
|
+
const current = saved.current || saved;
|
|
2389
|
+
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.");
|
|
2390
|
+
return { success: true, agent_targets: current.settings.agent_targets, updated_at: current.settings.updated_at };
|
|
2391
|
+
}
|
|
2128
2392
|
return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
|
|
2129
2393
|
method: "PATCH",
|
|
2130
2394
|
body: {
|
|
@@ -2443,22 +2707,27 @@ function selectCloudSkillsToApply(skills) {
|
|
|
2443
2707
|
}
|
|
2444
2708
|
return [...winners.values()];
|
|
2445
2709
|
}
|
|
2446
|
-
function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames,
|
|
2710
|
+
function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudSkills = []) {
|
|
2447
2711
|
return localSkills.filter((skill) => {
|
|
2448
2712
|
if (cloudCuratedSkillNames.has(skill.name)) {
|
|
2449
2713
|
return false;
|
|
2450
2714
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2715
|
+
const previous = syncState.skills[skill.name] || (skill.cloudId ? Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId) : void 0);
|
|
2716
|
+
const native = cloudSkills.find((row) => isNativeSkill(row) && row.id === (skill.cloudId || previous?.cloudId));
|
|
2717
|
+
if (native || skill.nativeReference || previous?.nativeReference || previous?.nativeUnavailable) {
|
|
2718
|
+
return Boolean(native && skill.nativeReference && skill.nativeScope === native.native_scope && (previous ? previous.folderHash !== skill.folderHash : skill.nativeBaseFolderHash !== skill.folderHash));
|
|
2453
2719
|
}
|
|
2454
|
-
const previous = syncState.skills[skill.name];
|
|
2455
2720
|
if (!previous) {
|
|
2721
|
+
if (hasNativeFolderHistory(syncState, skill.name) || /^native-[a-f0-9-]{36}$/.test(skill.name)) return false;
|
|
2456
2722
|
return true;
|
|
2457
2723
|
}
|
|
2458
2724
|
if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
|
|
2459
2725
|
return false;
|
|
2460
2726
|
}
|
|
2461
2727
|
return previous.folderHash !== skill.folderHash;
|
|
2728
|
+
}).map((skill) => {
|
|
2729
|
+
const previous = syncState.skills[skill.name] || Object.values(syncState.skills).find((row) => row.cloudId === skill.cloudId);
|
|
2730
|
+
return { ...skill, ...skill.cloudId || previous?.cloudId ? { cloudId: skill.cloudId || previous.cloudId } : {} };
|
|
2462
2731
|
});
|
|
2463
2732
|
}
|
|
2464
2733
|
async function writeCloudSkillWithBundleFallback(skill, dependencies) {
|
|
@@ -2530,6 +2799,24 @@ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
|
|
|
2530
2799
|
removeForeignAccountSymlinks,
|
|
2531
2800
|
updateAgentTargets
|
|
2532
2801
|
};
|
|
2802
|
+
function isolatedLinkOptions(paths) {
|
|
2803
|
+
if (!paths.isolatedNamespace && !paths.homeDir) return void 0;
|
|
2804
|
+
const root = paths.isolatedNamespace ? join3(paths.syncRoot, "environments", paths.isolatedNamespace, "agents") : paths.homeDir;
|
|
2805
|
+
return {
|
|
2806
|
+
agentSkillDirs: {
|
|
2807
|
+
notis: paths.skillsDir,
|
|
2808
|
+
claude_code: join3(root, ".claude", "skills"),
|
|
2809
|
+
cursor: join3(root, ".cursor", "skills"),
|
|
2810
|
+
codex: join3(root, ".codex", "skills")
|
|
2811
|
+
},
|
|
2812
|
+
legacyGlobalSkillsDir: join3(root, ".agents", "skills")
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
function assertSyncEnvironment(pull, settings) {
|
|
2816
|
+
if ((pull.sync_namespace || null) !== (settings.sync_namespace || null) || settings.sync_scope !== void 0 && pull.sync_scope !== settings.sync_scope) {
|
|
2817
|
+
throw new Error("Skill sync environment changed. Existing mirrors were retained.");
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2533
2820
|
function toSkillMap(skills) {
|
|
2534
2821
|
return new Map(skills.map((skill) => [skill.name, skill]));
|
|
2535
2822
|
}
|
|
@@ -2546,7 +2833,7 @@ function decodeJwtSubject(jwt) {
|
|
|
2546
2833
|
}
|
|
2547
2834
|
}
|
|
2548
2835
|
function cloudContentHash(skill) {
|
|
2549
|
-
return
|
|
2836
|
+
return createHash4("sha256").update(JSON.stringify({
|
|
2550
2837
|
md: skill.skill_md,
|
|
2551
2838
|
hash: skill.skill_folder_hash,
|
|
2552
2839
|
source: skill.skill_source_url,
|
|
@@ -2561,6 +2848,12 @@ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
|
|
|
2561
2848
|
if (!localSkill) {
|
|
2562
2849
|
return true;
|
|
2563
2850
|
}
|
|
2851
|
+
if (isNativeSkill(cloudSkill)) {
|
|
2852
|
+
if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
|
|
2853
|
+
if (nativeLocalReady(cloudSkill, localSkill)) return false;
|
|
2854
|
+
const nativePrevious = previousNativeState(previousState, cloudSkill);
|
|
2855
|
+
return localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash || !localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id && nativePrevious.folderHash === localSkill.folderHash;
|
|
2856
|
+
}
|
|
2564
2857
|
const previous = previousState.skills[skillName];
|
|
2565
2858
|
if (previous?.folderHash === localSkill.folderHash && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
|
|
2566
2859
|
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) {
|
|
@@ -2586,17 +2879,28 @@ function collectAppliedFiles(pullResponse, previousState, appliedFilesByName) {
|
|
|
2586
2879
|
}
|
|
2587
2880
|
return applied;
|
|
2588
2881
|
}
|
|
2589
|
-
function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}) {
|
|
2882
|
+
function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}, appliedFiles = {}, previousState = { version: 1, lastSyncedAt: null, skills: {} }) {
|
|
2590
2883
|
const localSkillMap = toSkillMap(localSkills);
|
|
2591
2884
|
const skills = Object.fromEntries(
|
|
2592
2885
|
selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
|
|
2593
2886
|
const localSkill = localSkillMap.get(skill.name);
|
|
2594
2887
|
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
2595
2888
|
const appliedFileList = appliedFiles[skill.name];
|
|
2889
|
+
if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
|
|
2890
|
+
const previous = previousNativeState(previousState, skill);
|
|
2891
|
+
return [skill.name, { ...previous || {
|
|
2892
|
+
cloudId: skill.id,
|
|
2893
|
+
folderHash: localSkill?.nativeBaseFolderHash || "",
|
|
2894
|
+
agentTargets: normalizeAgentTargets(skill.agent_targets),
|
|
2895
|
+
syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2896
|
+
...localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}
|
|
2897
|
+
}, nativeUnavailable: true, verifiedAgentLinks: {} }];
|
|
2898
|
+
}
|
|
2596
2899
|
return [
|
|
2597
2900
|
skill.name,
|
|
2598
2901
|
{
|
|
2599
2902
|
cloudId: skill.id,
|
|
2903
|
+
...nativeState(skill),
|
|
2600
2904
|
folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
|
|
2601
2905
|
agentTargets: normalizeAgentTargets(skill.agent_targets),
|
|
2602
2906
|
verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
|
|
@@ -2609,17 +2913,30 @@ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLi
|
|
|
2609
2913
|
];
|
|
2610
2914
|
})
|
|
2611
2915
|
);
|
|
2612
|
-
|
|
2916
|
+
for (const local of localSkills) {
|
|
2917
|
+
if (!local.nativeReference || Object.values(skills).some((row) => row.cloudId === local.cloudId)) continue;
|
|
2918
|
+
skills[local.name] = {
|
|
2919
|
+
cloudId: local.cloudId,
|
|
2920
|
+
nativeReference: local.nativeReference,
|
|
2921
|
+
nativeScope: local.nativeScope,
|
|
2922
|
+
nativeUnavailable: true,
|
|
2923
|
+
verifiedAgentLinks: {},
|
|
2924
|
+
folderHash: local.nativeBaseFolderHash || "",
|
|
2925
|
+
agentTargets: normalizeAgentTargets(),
|
|
2926
|
+
syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
return retainMissingNativeState(previousState, {
|
|
2613
2930
|
version: 1,
|
|
2614
2931
|
lastSyncedAt,
|
|
2615
2932
|
skills
|
|
2616
|
-
};
|
|
2933
|
+
});
|
|
2617
2934
|
}
|
|
2618
2935
|
function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
|
|
2619
2936
|
const cloudSkillNames = new Set(
|
|
2620
2937
|
pullResponse.skills.map((skill) => skill.name)
|
|
2621
2938
|
);
|
|
2622
|
-
const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).map((skill) => {
|
|
2939
|
+
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) => {
|
|
2623
2940
|
const previous = previousState.skills[skill.name];
|
|
2624
2941
|
return {
|
|
2625
2942
|
id: previous?.cloudId || `local-${skill.name}`,
|
|
@@ -2632,7 +2949,7 @@ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
|
|
|
2632
2949
|
status: "active"
|
|
2633
2950
|
};
|
|
2634
2951
|
});
|
|
2635
|
-
return [...pullResponse
|
|
2952
|
+
return [...nativeLinkRows(pullResponse, localSkills), ...localOnlySkills];
|
|
2636
2953
|
}
|
|
2637
2954
|
function isEmptySyncState(state) {
|
|
2638
2955
|
return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
|
|
@@ -2659,14 +2976,19 @@ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
|
|
|
2659
2976
|
skills: migratedSkills
|
|
2660
2977
|
};
|
|
2661
2978
|
}
|
|
2662
|
-
async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}) {
|
|
2979
|
+
async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps, failures = [], writtenSkillNames = /* @__PURE__ */ new Set(), appliedFilesByName = {}, acknowledgedNative = /* @__PURE__ */ new Map()) {
|
|
2663
2980
|
const localSkillMap = toSkillMap(localSkills);
|
|
2664
2981
|
const warnSkillSync = (message, error) => {
|
|
2665
2982
|
console.warn(`[Notis] ${message}`, error);
|
|
2666
2983
|
};
|
|
2667
2984
|
let downloaded = 0;
|
|
2668
2985
|
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
2669
|
-
|
|
2986
|
+
const local = localSkillMap.get(cloudSkill.name);
|
|
2987
|
+
const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
|
|
2988
|
+
if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
2989
|
+
if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
|
|
2990
|
+
failures.push({ name: cloudSkill.name, error: "Local native Skill changes were retained; resolve the version or identity conflict before using them." });
|
|
2991
|
+
}
|
|
2670
2992
|
continue;
|
|
2671
2993
|
}
|
|
2672
2994
|
const outcome = await writeCloudSkillWithBundleFallback(cloudSkill, {
|
|
@@ -2674,7 +2996,8 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
|
|
|
2674
2996
|
writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
|
|
2675
2997
|
// What the last write put there. Everything else in the folder was
|
|
2676
2998
|
// produced locally and survives this one.
|
|
2677
|
-
previouslyApplied: previousState
|
|
2999
|
+
previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
|
|
3000
|
+
...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}
|
|
2678
3001
|
}),
|
|
2679
3002
|
onWarning: warnSkillSync
|
|
2680
3003
|
});
|
|
@@ -2691,20 +3014,26 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
|
|
|
2691
3014
|
return downloaded;
|
|
2692
3015
|
}
|
|
2693
3016
|
function assertSkillsPullAuthorized(pullResponse) {
|
|
3017
|
+
if (!Array.isArray(pullResponse.skills)) throw new Error("Skill inventory is unavailable; existing files were retained.");
|
|
3018
|
+
assertNativePull(pullResponse);
|
|
2694
3019
|
if (pullResponse.entitlement_access?.code === "entitlement_upgrade_required" && pullResponse.entitlement_access.entitlement === "skills") {
|
|
2695
3020
|
throw new Error(
|
|
2696
3021
|
"Skill sync access was denied; preserving existing local skills."
|
|
2697
3022
|
);
|
|
2698
3023
|
}
|
|
2699
3024
|
}
|
|
2700
|
-
async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures) {
|
|
3025
|
+
async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps, failures, linkOptions) {
|
|
2701
3026
|
if (isEmptySyncState(scopedState)) {
|
|
2702
3027
|
return 0;
|
|
2703
3028
|
}
|
|
2704
3029
|
const deletions = await deps.detectDeletedAgentSymlinks(
|
|
2705
|
-
pullResponse.skills
|
|
3030
|
+
pullResponse.skills.map((skill) => {
|
|
3031
|
+
const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : void 0;
|
|
3032
|
+
return previous ? { ...skill, name: previous[0] } : skill;
|
|
3033
|
+
}),
|
|
2706
3034
|
previousState,
|
|
2707
|
-
skillsDir
|
|
3035
|
+
skillsDir,
|
|
3036
|
+
linkOptions
|
|
2708
3037
|
);
|
|
2709
3038
|
if (deletions.length === 0) {
|
|
2710
3039
|
return 0;
|
|
@@ -2720,21 +3049,34 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
|
|
|
2720
3049
|
}
|
|
2721
3050
|
const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
2722
3051
|
assertSkillsPullAuthorized(fresh);
|
|
3052
|
+
assertSyncEnvironment(fresh, pullResponse);
|
|
2723
3053
|
Object.assign(pullResponse, fresh);
|
|
2724
3054
|
let needsRefresh = false;
|
|
2725
3055
|
let deactivated = 0;
|
|
2726
3056
|
for (const [skillId, { skillName, agents }] of agentsBySkill) {
|
|
2727
3057
|
const skill = pullResponse.skills.find((item) => item.id === skillId);
|
|
2728
3058
|
const previous = previousState.skills[skillName];
|
|
2729
|
-
if (!skill
|
|
3059
|
+
if (!skill) continue;
|
|
3060
|
+
const native = isNativeSkill(skill);
|
|
3061
|
+
if (native) {
|
|
3062
|
+
if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
|
|
3063
|
+
} else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
|
|
2730
3064
|
const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
|
|
2731
3065
|
try {
|
|
2732
|
-
const saved = await deps.updateAgentTargets(
|
|
2733
|
-
|
|
3066
|
+
const saved = await deps.updateAgentTargets(
|
|
3067
|
+
serverUrl,
|
|
3068
|
+
jwt,
|
|
3069
|
+
skillId,
|
|
3070
|
+
patch,
|
|
3071
|
+
native ? void 0 : skill.updated_at,
|
|
3072
|
+
native ? { reference: skill.native_reference, settingsRevision: skill.native_settings.revision } : void 0
|
|
3073
|
+
);
|
|
3074
|
+
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)) {
|
|
2734
3075
|
throw new Error("Assignment update did not return a verified saved revision");
|
|
2735
3076
|
}
|
|
2736
3077
|
skill.agent_targets = saved.agent_targets;
|
|
2737
3078
|
skill.updated_at = saved.updated_at;
|
|
3079
|
+
if (native) needsRefresh = true;
|
|
2738
3080
|
deactivated += agents.size;
|
|
2739
3081
|
} catch (error) {
|
|
2740
3082
|
needsRefresh = true;
|
|
@@ -2745,6 +3087,7 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
|
|
|
2745
3087
|
if (needsRefresh) {
|
|
2746
3088
|
const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
2747
3089
|
assertSkillsPullAuthorized(refreshed);
|
|
3090
|
+
assertSyncEnvironment(refreshed, pullResponse);
|
|
2748
3091
|
Object.assign(pullResponse, refreshed);
|
|
2749
3092
|
}
|
|
2750
3093
|
return deactivated;
|
|
@@ -2764,11 +3107,12 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2764
3107
|
"Cannot sync skills without a server-verified account identity."
|
|
2765
3108
|
);
|
|
2766
3109
|
}
|
|
2767
|
-
const syncPaths = getSkillSyncPathsForUser(syncUserId);
|
|
2768
|
-
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
2769
|
-
syncPaths.skillsDir
|
|
2770
|
-
);
|
|
3110
|
+
const syncPaths = getSkillSyncPathsForUser(syncUserId, void 0, syncSettings.sync_namespace, options.syncHome);
|
|
2771
3111
|
if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
|
|
3112
|
+
const foreignLinksRemoved2 = await deps.removeForeignAccountSymlinks(
|
|
3113
|
+
syncPaths.skillsDir,
|
|
3114
|
+
isolatedLinkOptions(syncPaths)
|
|
3115
|
+
);
|
|
2772
3116
|
return {
|
|
2773
3117
|
syncEnabled: false,
|
|
2774
3118
|
pushed: 0,
|
|
@@ -2777,7 +3121,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2777
3121
|
deleted: 0,
|
|
2778
3122
|
deactivated: 0,
|
|
2779
3123
|
linked: 0,
|
|
2780
|
-
removed:
|
|
3124
|
+
removed: foreignLinksRemoved2,
|
|
2781
3125
|
skipped: 0,
|
|
2782
3126
|
lastSyncedAt: syncSettings.last_synced_at,
|
|
2783
3127
|
failedPushes: []
|
|
@@ -2785,13 +3129,18 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2785
3129
|
}
|
|
2786
3130
|
let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
2787
3131
|
assertSkillsPullAuthorized(pullResponse);
|
|
3132
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
3133
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
3134
|
+
syncPaths.skillsDir,
|
|
3135
|
+
isolatedLinkOptions(syncPaths)
|
|
3136
|
+
);
|
|
2788
3137
|
const cloudCuratedSkillNames = new Set(
|
|
2789
3138
|
pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
|
|
2790
3139
|
);
|
|
2791
3140
|
const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
|
|
2792
3141
|
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
2793
3142
|
const assignmentFailures = [];
|
|
2794
|
-
const deactivated = syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
|
|
3143
|
+
const deactivated = !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
|
|
2795
3144
|
serverUrl,
|
|
2796
3145
|
jwt,
|
|
2797
3146
|
pullResponse,
|
|
@@ -2799,41 +3148,55 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2799
3148
|
scopedState,
|
|
2800
3149
|
syncPaths.skillsDir,
|
|
2801
3150
|
deps,
|
|
2802
|
-
assignmentFailures
|
|
3151
|
+
assignmentFailures,
|
|
3152
|
+
isolatedLinkOptions(syncPaths)
|
|
2803
3153
|
) : 0;
|
|
3154
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
2804
3155
|
const authUserId = decodeJwtSubject(jwt);
|
|
2805
3156
|
let previousAuthState = null;
|
|
2806
|
-
if (authUserId && authUserId !== syncUserId) {
|
|
2807
|
-
const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
|
|
3157
|
+
if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
|
|
3158
|
+
const previousAuthPaths = getSkillSyncPathsForUser(authUserId, void 0, void 0, options.syncHome);
|
|
2808
3159
|
previousAuthState = await deps.readSyncState(previousAuthPaths);
|
|
2809
3160
|
await deps.gatherTopLevelLocalSkills(syncPaths, {
|
|
2810
3161
|
sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
|
|
2811
3162
|
protectedSkillNames
|
|
2812
3163
|
});
|
|
2813
3164
|
}
|
|
2814
|
-
|
|
2815
|
-
protectedSkillNames
|
|
2816
|
-
}
|
|
3165
|
+
if (!syncPaths.isolatedNamespace) {
|
|
3166
|
+
await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
|
|
3167
|
+
}
|
|
2817
3168
|
const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
2818
3169
|
const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
|
|
2819
3170
|
localSkills,
|
|
2820
3171
|
scopedState,
|
|
2821
|
-
isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
|
|
3172
|
+
!syncPaths.isolatedNamespace && isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
|
|
2822
3173
|
));
|
|
3174
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
2823
3175
|
const gatheredSymlinkResult = await deps.syncSymlinks(
|
|
2824
3176
|
buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
|
|
2825
|
-
syncPaths.skillsDir
|
|
3177
|
+
syncPaths.skillsDir,
|
|
3178
|
+
isolatedLinkOptions(syncPaths)
|
|
2826
3179
|
);
|
|
2827
3180
|
const pushCandidates = getPushCandidates(
|
|
2828
3181
|
localSkills,
|
|
2829
3182
|
previousState,
|
|
2830
3183
|
cloudCuratedSkillNames,
|
|
2831
3184
|
new Set(pullResponse.skills.map((skill) => skill.name)),
|
|
2832
|
-
|
|
3185
|
+
pullResponse.skills
|
|
2833
3186
|
);
|
|
2834
3187
|
const failedPushes = [];
|
|
3188
|
+
const acknowledgedNative = /* @__PURE__ */ new Map();
|
|
3189
|
+
let writeLocalSkills = localSkills;
|
|
2835
3190
|
if (pushCandidates.length > 0) {
|
|
2836
3191
|
const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
|
|
3192
|
+
for (const result of pushResult.skills || []) {
|
|
3193
|
+
const saved = result;
|
|
3194
|
+
const candidate = pushCandidates.find((local) => local.nativeReference && local.cloudId === saved.id);
|
|
3195
|
+
if (candidate && saved.native_reference) {
|
|
3196
|
+
checkedNativeReference(saved.native_reference, candidate.cloudId);
|
|
3197
|
+
acknowledgedNative.set(candidate.cloudId, candidate.folderHash);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
2837
3200
|
if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
|
|
2838
3201
|
failedPushes.push(...pushResult.failed);
|
|
2839
3202
|
console.warn(
|
|
@@ -2842,13 +3205,21 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2842
3205
|
}
|
|
2843
3206
|
pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
2844
3207
|
assertSkillsPullAuthorized(pullResponse);
|
|
3208
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
3209
|
+
writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
3210
|
+
pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
|
|
2845
3211
|
}
|
|
2846
3212
|
const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
|
|
2847
3213
|
let deleted = 0;
|
|
2848
3214
|
for (const skillName of Object.keys(previousState.skills)) {
|
|
2849
3215
|
if (!cloudSkillNames.has(skillName)) {
|
|
3216
|
+
const previous = previousState.skills[skillName];
|
|
3217
|
+
if (previous.nativeReference || previous.nativeUnavailable || pullResponse.skills.some((skill) => isNativeSkill(skill) && skill.id === previous.cloudId)) {
|
|
3218
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
2850
3221
|
await deps.deleteLocalSkill(skillName, syncPaths);
|
|
2851
|
-
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
|
|
3222
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
2852
3223
|
deleted += 1;
|
|
2853
3224
|
}
|
|
2854
3225
|
}
|
|
@@ -2857,18 +3228,20 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2857
3228
|
const appliedFilesByName = {};
|
|
2858
3229
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
2859
3230
|
pullResponse,
|
|
2860
|
-
|
|
3231
|
+
writeLocalSkills,
|
|
2861
3232
|
previousState,
|
|
2862
3233
|
syncPaths,
|
|
2863
3234
|
deps,
|
|
2864
3235
|
failedDownloads,
|
|
2865
3236
|
writtenSkillNames,
|
|
2866
|
-
appliedFilesByName
|
|
3237
|
+
appliedFilesByName,
|
|
3238
|
+
acknowledgedNative
|
|
2867
3239
|
);
|
|
2868
3240
|
const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
2869
3241
|
const symlinkResult = await deps.syncSymlinks(
|
|
2870
3242
|
buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
|
|
2871
|
-
syncPaths.skillsDir
|
|
3243
|
+
syncPaths.skillsDir,
|
|
3244
|
+
isolatedLinkOptions(syncPaths)
|
|
2872
3245
|
);
|
|
2873
3246
|
const verifiedLinks = { ...symlinkResult.verifiedAgentLinks ?? {} };
|
|
2874
3247
|
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
@@ -2881,7 +3254,8 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2881
3254
|
verifiedLinks,
|
|
2882
3255
|
new Set(failedDownloads.map((item) => item.name)),
|
|
2883
3256
|
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
2884
|
-
collectAppliedFiles(pullResponse, previousState, appliedFilesByName)
|
|
3257
|
+
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
3258
|
+
previousState
|
|
2885
3259
|
),
|
|
2886
3260
|
syncPaths
|
|
2887
3261
|
);
|
|
@@ -2904,13 +3278,13 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2904
3278
|
}
|
|
2905
3279
|
|
|
2906
3280
|
// src/runtime/sync-skills.js
|
|
2907
|
-
import { createHash as
|
|
3281
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
|
|
2908
3282
|
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2909
3283
|
import { homedir as homedir3 } from "node:os";
|
|
2910
|
-
import { dirname as dirname4, join as
|
|
3284
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
2911
3285
|
|
|
2912
3286
|
// src/runtime/base-skills.js
|
|
2913
|
-
import { dirname as dirname3, join as
|
|
3287
|
+
import { dirname as dirname3, join as join4, relative, resolve as resolve2 } from "node:path";
|
|
2914
3288
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2915
3289
|
var BASE_SKILL_NAMES2 = Object.freeze([
|
|
2916
3290
|
"notis-apps",
|
|
@@ -2926,7 +3300,7 @@ var DEFAULT_LOCK_POLL_MS = 50;
|
|
|
2926
3300
|
async function lockSnapshot(lockDirectory) {
|
|
2927
3301
|
try {
|
|
2928
3302
|
const [raw, metadata] = await Promise.all([
|
|
2929
|
-
readFile(
|
|
3303
|
+
readFile(join5(lockDirectory, "owner"), "utf8").catch(() => ""),
|
|
2930
3304
|
stat(lockDirectory)
|
|
2931
3305
|
]);
|
|
2932
3306
|
let owner = {};
|
|
@@ -2962,11 +3336,11 @@ function delay(milliseconds) {
|
|
|
2962
3336
|
return new Promise((resolve3) => setTimeout(resolve3, milliseconds));
|
|
2963
3337
|
}
|
|
2964
3338
|
async function quarantineStaleLock(lockDirectory, snapshot) {
|
|
2965
|
-
const quarantineRoot =
|
|
2966
|
-
const snapshotToken =
|
|
3339
|
+
const quarantineRoot = join5(dirname4(lockDirectory), ".stale-operation-locks");
|
|
3340
|
+
const snapshotToken = createHash5("sha256").update(JSON.stringify(snapshot)).digest("hex");
|
|
2967
3341
|
await mkdir(quarantineRoot, { recursive: true, mode: 448 });
|
|
2968
3342
|
try {
|
|
2969
|
-
await rename(lockDirectory,
|
|
3343
|
+
await rename(lockDirectory, join5(quarantineRoot, snapshotToken));
|
|
2970
3344
|
return true;
|
|
2971
3345
|
} catch (error) {
|
|
2972
3346
|
if (["EEXIST", "ENOTEMPTY", "ENOENT"].includes(error?.code)) return false;
|
|
@@ -2974,16 +3348,16 @@ async function quarantineStaleLock(lockDirectory, snapshot) {
|
|
|
2974
3348
|
}
|
|
2975
3349
|
}
|
|
2976
3350
|
async function writeLockOwnerAtomically(lockDirectory, owner) {
|
|
2977
|
-
const temporaryOwnerPath =
|
|
2978
|
-
const ownerPath =
|
|
3351
|
+
const temporaryOwnerPath = join5(lockDirectory, `.owner.${owner.id}.tmp`);
|
|
3352
|
+
const ownerPath = join5(lockDirectory, "owner");
|
|
2979
3353
|
await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 384 });
|
|
2980
3354
|
await rename(temporaryOwnerPath, ownerPath);
|
|
2981
3355
|
}
|
|
2982
3356
|
async function releaseOwnedLock(lockDirectory, ownerId) {
|
|
2983
3357
|
const owner = await lockSnapshot(lockDirectory);
|
|
2984
3358
|
if (owner?.id !== ownerId) return;
|
|
2985
|
-
const quarantineRoot =
|
|
2986
|
-
const releasedDirectory =
|
|
3359
|
+
const quarantineRoot = join5(dirname4(lockDirectory), ".stale-operation-locks");
|
|
3360
|
+
const releasedDirectory = join5(quarantineRoot, `released.${ownerId}`);
|
|
2987
3361
|
await mkdir(quarantineRoot, { recursive: true, mode: 448 });
|
|
2988
3362
|
try {
|
|
2989
3363
|
await rename(lockDirectory, releasedDirectory);
|
|
@@ -3000,7 +3374,7 @@ async function withSkillSyncLock(callback, {
|
|
|
3000
3374
|
pollMs = DEFAULT_LOCK_POLL_MS,
|
|
3001
3375
|
now = () => Date.now()
|
|
3002
3376
|
} = {}) {
|
|
3003
|
-
const lockDirectory =
|
|
3377
|
+
const lockDirectory = join5(home, ".notis", "skills", ".operation-lock");
|
|
3004
3378
|
const ownerId = `${process.pid}.${randomUUID2()}`;
|
|
3005
3379
|
const deadline = now() + timeoutMs;
|
|
3006
3380
|
await mkdir(dirname4(lockDirectory), { recursive: true, mode: 448 });
|
|
@@ -3039,7 +3413,7 @@ async function withSkillSyncLock(callback, {
|
|
|
3039
3413
|
let heartbeatInFlight = Promise.resolve();
|
|
3040
3414
|
const refreshHeartbeat = async () => {
|
|
3041
3415
|
if (heartbeatStopped) return;
|
|
3042
|
-
const temporaryOwnerPath =
|
|
3416
|
+
const temporaryOwnerPath = join5(lockDirectory, `.owner.${ownerId}.tmp`);
|
|
3043
3417
|
await writeFile(
|
|
3044
3418
|
temporaryOwnerPath,
|
|
3045
3419
|
JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
|
|
@@ -3050,7 +3424,7 @@ async function withSkillSyncLock(callback, {
|
|
|
3050
3424
|
await rm(temporaryOwnerPath, { force: true });
|
|
3051
3425
|
return;
|
|
3052
3426
|
}
|
|
3053
|
-
await rename(temporaryOwnerPath,
|
|
3427
|
+
await rename(temporaryOwnerPath, join5(lockDirectory, "owner"));
|
|
3054
3428
|
};
|
|
3055
3429
|
const heartbeat = setInterval(() => {
|
|
3056
3430
|
heartbeatInFlight = heartbeatInFlight.then(refreshHeartbeat).catch(() => void 0);
|
|
@@ -3087,10 +3461,10 @@ async function runAutomaticSkillSync({ profile, apiBase, userId }, {
|
|
|
3087
3461
|
}
|
|
3088
3462
|
async function main(args = process.argv.slice(2)) {
|
|
3089
3463
|
const [profile, apiBase, userId] = args;
|
|
3090
|
-
const root =
|
|
3464
|
+
const root = join6(homedir4(), ".notis", "skills", "service");
|
|
3091
3465
|
const record = (value) => {
|
|
3092
3466
|
mkdirSync3(root, { recursive: true, mode: 448 });
|
|
3093
|
-
const target =
|
|
3467
|
+
const target = join6(root, "status.json");
|
|
3094
3468
|
const temporary = `${target}.${process.pid}.tmp`;
|
|
3095
3469
|
writeFileSync3(temporary, JSON.stringify({ ...value, profile, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), { mode: 384 });
|
|
3096
3470
|
renameSync3(temporary, target);
|