@notis_ai/cli 0.2.0-beta.136.1 → 0.2.0-beta.139.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +38 -0
  2. package/dist/agent-hooks/notis-agent-hook.mjs +16620 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +9 -6
  4. package/{skills → dist/base-skills}/notis-cli/SKILL.md +1 -1
  5. package/dist/base-skills/notis-query/SKILL.md +705 -0
  6. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +8 -0
  7. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +8 -0
  8. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +8 -0
  9. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +8 -0
  10. package/dist/skill-sync/index.js +1528 -0
  11. package/dist/skill-sync/index.js.map +7 -0
  12. package/package.json +4 -1
  13. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  14. package/skills/notis-onboarding/BRIEF.md +16 -0
  15. package/src/agent-hook-entry.js +5 -0
  16. package/src/cli.js +23 -14
  17. package/src/command-specs/agents.js +392 -0
  18. package/src/command-specs/auth.js +16 -0
  19. package/src/command-specs/index.js +6 -0
  20. package/src/command-specs/onboarding.js +59 -2
  21. package/src/command-specs/skills.js +56 -0
  22. package/src/runtime/agent-memory-state.js +126 -0
  23. package/src/runtime/agent-setup.js +383 -0
  24. package/src/runtime/base-skills.d.ts +20 -0
  25. package/src/runtime/base-skills.js +167 -0
  26. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  27. package/src/runtime/skill-sync/index.ts +644 -0
  28. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  29. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  30. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  31. package/src/runtime/skill-sync/types.ts +103 -0
  32. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  33. package/src/runtime/store-screenshot.js +6 -1
  34. package/src/runtime/sync-skills.d.ts +37 -0
  35. package/src/runtime/sync-skills.js +215 -0
  36. package/template/packages/sdk/src/config.ts +8 -0
@@ -0,0 +1,1528 @@
1
+ // src/runtime/skill-sync/types.ts
2
+ var DEFAULT_AGENT_TARGETS = {
3
+ notis: true,
4
+ claude_code: true,
5
+ cursor: true,
6
+ codex: true
7
+ };
8
+ function normalizeAgentTargets(targets) {
9
+ return {
10
+ notis: Boolean(targets?.notis ?? DEFAULT_AGENT_TARGETS.notis),
11
+ claude_code: Boolean(targets?.claude_code ?? DEFAULT_AGENT_TARGETS.claude_code),
12
+ cursor: Boolean(targets?.cursor ?? DEFAULT_AGENT_TARGETS.cursor),
13
+ codex: Boolean(targets?.codex ?? DEFAULT_AGENT_TARGETS.codex)
14
+ };
15
+ }
16
+
17
+ // src/runtime/skill-sync/local-scanner.ts
18
+ import { createHash } from "crypto";
19
+ import { execFile } from "child_process";
20
+ import { promises as fs } from "fs";
21
+ import os from "os";
22
+ import path from "path";
23
+ import { promisify } from "util";
24
+ var HOME_DIR = os.homedir();
25
+ var execFileAsync = promisify(execFile);
26
+ var AGENTS_DIR = path.join(HOME_DIR, ".agents");
27
+ var LEGACY_AGENTS_SKILLS_DIR = path.join(AGENTS_DIR, "skills");
28
+ var NOTIS_SKILL_SYNC_ROOT = path.join(HOME_DIR, ".notis", "skills");
29
+ var LEGACY_NOTIS_SYNC_STATE_PATH = path.join(
30
+ AGENTS_DIR,
31
+ ".notis-sync.json"
32
+ );
33
+ var AGENTS_SKILLS_DIR = LEGACY_AGENTS_SKILLS_DIR;
34
+ var SKILL_LOCK_PATH = path.join(AGENTS_DIR, ".skill-lock.json");
35
+ var DEFAULT_SYNC_STATE = {
36
+ version: 1,
37
+ lastSyncedAt: null,
38
+ skills: {}
39
+ };
40
+ var EXCLUDED_TOP_LEVEL_ROOT_NAMES = /* @__PURE__ */ new Set([
41
+ "backup",
42
+ "backups",
43
+ "builtin",
44
+ "builtins",
45
+ "cache",
46
+ "caches",
47
+ "marketplace",
48
+ "marketplaces",
49
+ "plugin",
50
+ "plugins",
51
+ "temp",
52
+ "tmp",
53
+ "worktree",
54
+ "worktrees"
55
+ ]);
56
+ var DEFAULT_SYNC_PATHS = {
57
+ agentsDir: AGENTS_DIR,
58
+ syncRoot: NOTIS_SKILL_SYNC_ROOT,
59
+ legacySkillsDir: LEGACY_AGENTS_SKILLS_DIR,
60
+ legacyScopedSkillsDir: LEGACY_AGENTS_SKILLS_DIR,
61
+ legacyScopedSyncStatePath: LEGACY_NOTIS_SYNC_STATE_PATH,
62
+ skillsDir: LEGACY_AGENTS_SKILLS_DIR,
63
+ syncStatePath: LEGACY_NOTIS_SYNC_STATE_PATH,
64
+ skillLockPath: SKILL_LOCK_PATH,
65
+ gatherMetadataPath: path.join(NOTIS_SKILL_SYNC_ROOT, "skill-gather-metadata.json")
66
+ };
67
+ function getDefaultSyncRootForAgentsDir(resolvedAgentsDir) {
68
+ if (resolvedAgentsDir === path.resolve(AGENTS_DIR)) {
69
+ return NOTIS_SKILL_SYNC_ROOT;
70
+ }
71
+ return path.join(resolvedAgentsDir, ".notis", "skills");
72
+ }
73
+ function isResolvedChildPath(baseDir, candidatePath) {
74
+ return candidatePath.startsWith(`${baseDir}${path.sep}`);
75
+ }
76
+ function sanitizePathSegment(value) {
77
+ const raw = value.trim();
78
+ const sanitized = raw.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
79
+ if (!sanitized) {
80
+ throw new Error("Invalid sync user id");
81
+ }
82
+ if (sanitized === raw) {
83
+ return sanitized;
84
+ }
85
+ return `${sanitized}-${createHash("sha256").update(raw).digest("hex").slice(0, 12)}`;
86
+ }
87
+ function getSkillSyncPathsForUser(authUserId, agentsDir = AGENTS_DIR) {
88
+ const safeUserId = sanitizePathSegment(authUserId);
89
+ const resolvedAgentsDir = path.resolve(agentsDir);
90
+ const syncRoot = getDefaultSyncRootForAgentsDir(resolvedAgentsDir);
91
+ const userRoot = path.join(syncRoot, "users", safeUserId);
92
+ const legacyUserRoot = path.join(resolvedAgentsDir, "notis", "users", safeUserId);
93
+ return {
94
+ agentsDir: resolvedAgentsDir,
95
+ syncRoot,
96
+ legacySkillsDir: path.join(resolvedAgentsDir, "skills"),
97
+ legacyScopedSkillsDir: path.join(legacyUserRoot, "skills"),
98
+ legacyScopedSyncStatePath: path.join(legacyUserRoot, ".notis-sync.json"),
99
+ skillsDir: path.join(userRoot, "skills"),
100
+ syncStatePath: path.join(userRoot, ".notis-sync.json"),
101
+ skillLockPath: path.join(resolvedAgentsDir, ".skill-lock.json"),
102
+ gatherMetadataPath: path.join(userRoot, ".notis-gathered-skills.json")
103
+ };
104
+ }
105
+ function assertRelativeBundlePath(relativePath) {
106
+ const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
107
+ if (!normalized || normalized.startsWith("../") || `/${normalized}/`.includes("/../")) {
108
+ throw new Error(`Invalid bundle file path: ${relativePath}`);
109
+ }
110
+ return normalized;
111
+ }
112
+ function stripWrappingQuotes(value) {
113
+ return value.replace(/^['"]|['"]$/g, "").trim();
114
+ }
115
+ function parseFrontMatter(skillMd) {
116
+ const match = skillMd.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
117
+ if (!match) {
118
+ return { description: "" };
119
+ }
120
+ let description = "";
121
+ for (const line of match[1].split("\n")) {
122
+ const trimmed = line.trim();
123
+ if (!trimmed || trimmed.startsWith("#")) {
124
+ continue;
125
+ }
126
+ const descriptionMatch = trimmed.match(/^description\s*:\s*(.+)$/i);
127
+ if (descriptionMatch) {
128
+ description = stripWrappingQuotes(descriptionMatch[1]);
129
+ break;
130
+ }
131
+ }
132
+ return { description };
133
+ }
134
+ async function readJsonFile(filePath) {
135
+ try {
136
+ const raw = await fs.readFile(filePath, "utf8");
137
+ return JSON.parse(raw);
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ async function listFilesRecursive(dirPath) {
143
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
144
+ const nested = await Promise.all(
145
+ entries.map(async (entry) => {
146
+ if (entry.name === ".DS_Store") {
147
+ return [];
148
+ }
149
+ const fullPath = path.join(dirPath, entry.name);
150
+ if (entry.isDirectory()) {
151
+ return listFilesRecursive(fullPath);
152
+ }
153
+ if (entry.isFile()) {
154
+ return [fullPath];
155
+ }
156
+ return [];
157
+ })
158
+ );
159
+ return nested.flat().sort();
160
+ }
161
+ async function computeFolderHash(dirPath) {
162
+ const hash = createHash("sha256");
163
+ const filePaths = await listFilesRecursive(dirPath);
164
+ for (const filePath of filePaths) {
165
+ const relativePath = path.relative(dirPath, filePath);
166
+ hash.update(relativePath);
167
+ hash.update("\0");
168
+ hash.update(await fs.readFile(filePath));
169
+ hash.update("\0");
170
+ }
171
+ return hash.digest("hex");
172
+ }
173
+ function resolveSkillBundleFilePath(skillDir, relativePath) {
174
+ const normalizedPath = assertRelativeBundlePath(relativePath);
175
+ const resolvedSkillDir = path.resolve(skillDir);
176
+ const candidatePath = path.resolve(resolvedSkillDir, normalizedPath);
177
+ if (candidatePath === resolvedSkillDir || !isResolvedChildPath(resolvedSkillDir, candidatePath)) {
178
+ throw new Error(
179
+ `Bundle file resolves outside the expected directory: ${relativePath}`
180
+ );
181
+ }
182
+ return candidatePath;
183
+ }
184
+ async function readSkillMdIfValid(skillDir) {
185
+ try {
186
+ await fs.readFile(path.join(skillDir, "SKILL.md"), "utf8");
187
+ return true;
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+ async function moveDirectory(sourceDir, destinationDir) {
193
+ await fs.mkdir(path.dirname(destinationDir), { recursive: true });
194
+ try {
195
+ await fs.rename(sourceDir, destinationDir);
196
+ } catch (error) {
197
+ if (error.code !== "EXDEV") {
198
+ throw error;
199
+ }
200
+ await fs.cp(sourceDir, destinationDir, {
201
+ recursive: true,
202
+ errorOnExist: true,
203
+ force: false
204
+ });
205
+ await fs.rm(sourceDir, { recursive: true, force: true });
206
+ }
207
+ }
208
+ async function createDirectorySymlink(targetDir, linkPath) {
209
+ await fs.mkdir(path.dirname(linkPath), { recursive: true });
210
+ await fs.symlink(path.relative(path.dirname(linkPath), targetDir), linkPath, "dir");
211
+ }
212
+ function backupRootFor(paths, timestamp) {
213
+ return path.join(paths.syncRoot, "skill-dedupe-backups", timestamp);
214
+ }
215
+ function relativeBackupPath(label, skillName) {
216
+ return path.join(
217
+ label.replace(/[^A-Za-z0-9_.-]+/g, "_"),
218
+ safeName(skillName)
219
+ );
220
+ }
221
+ function isExcludedTopLevelSkillEntry(entryName) {
222
+ return entryName.startsWith(".") || isTransientSkillDirectoryName(entryName) || EXCLUDED_TOP_LEVEL_ROOT_NAMES.has(entryName.toLowerCase());
223
+ }
224
+ function isTransientSkillDirectoryName(entryName) {
225
+ return /\.(?:backup|staging)-/.test(entryName);
226
+ }
227
+ function defaultTopLevelSkillSources(paths) {
228
+ const sources = [];
229
+ if (path.resolve(paths.legacyScopedSkillsDir) !== path.resolve(paths.skillsDir)) {
230
+ sources.push({
231
+ label: "notis-legacy-scoped",
232
+ root: paths.legacyScopedSkillsDir,
233
+ priority: 1
234
+ });
235
+ }
236
+ sources.push(
237
+ { label: "agents", root: paths.legacySkillsDir, priority: 2 },
238
+ {
239
+ label: "codex",
240
+ root: path.join(HOME_DIR, ".codex", "skills"),
241
+ priority: 3
242
+ },
243
+ {
244
+ label: "cursor",
245
+ root: path.join(HOME_DIR, ".cursor", "skills"),
246
+ priority: 4
247
+ },
248
+ {
249
+ label: "claude",
250
+ root: path.join(HOME_DIR, ".claude", "skills"),
251
+ priority: 5
252
+ }
253
+ );
254
+ return sources;
255
+ }
256
+ function isManagedTopLevelSymlinkTarget(resolvedPath, paths) {
257
+ const managedRoots = [
258
+ path.resolve(paths.skillsDir),
259
+ path.resolve(paths.legacySkillsDir),
260
+ path.resolve(paths.legacyScopedSkillsDir),
261
+ path.join(path.resolve(paths.syncRoot), "base"),
262
+ path.join(path.resolve(paths.syncRoot), "users"),
263
+ path.join(path.resolve(paths.agentsDir), "notis", "users")
264
+ ];
265
+ return managedRoots.some(
266
+ (root) => resolvedPath === root || resolvedPath.startsWith(`${root}${path.sep}`)
267
+ );
268
+ }
269
+ async function listTopLevelSkillCandidates(paths, options) {
270
+ const sourceRoots = options.sourceRoots ? options.sourceRoots.map((source, index) => ({
271
+ ...source,
272
+ priority: index + 1
273
+ })) : defaultTopLevelSkillSources(paths);
274
+ const candidates = [];
275
+ try {
276
+ const scopedEntries = await fs.readdir(paths.skillsDir, {
277
+ withFileTypes: true
278
+ });
279
+ for (const entry of scopedEntries) {
280
+ if (!entry.isDirectory() && !entry.isSymbolicLink() || entry.name.startsWith(".") || isTransientSkillDirectoryName(entry.name)) {
281
+ continue;
282
+ }
283
+ const skillDir = path.join(paths.skillsDir, entry.name);
284
+ let resolvedPath = path.resolve(skillDir);
285
+ if (entry.isSymbolicLink()) {
286
+ try {
287
+ resolvedPath = path.resolve(path.dirname(skillDir), await fs.readlink(skillDir));
288
+ } catch {
289
+ continue;
290
+ }
291
+ }
292
+ if (await readSkillMdIfValid(skillDir)) {
293
+ candidates.push({
294
+ name: safeName(entry.name, paths.skillsDir),
295
+ root: paths.skillsDir,
296
+ label: "notis-managed",
297
+ path: skillDir,
298
+ resolvedPath,
299
+ priority: 0,
300
+ isScoped: true,
301
+ isSymlink: entry.isSymbolicLink()
302
+ });
303
+ }
304
+ }
305
+ } catch {
306
+ }
307
+ for (const source of sourceRoots) {
308
+ let entries;
309
+ try {
310
+ entries = await fs.readdir(source.root, { withFileTypes: true });
311
+ } catch {
312
+ continue;
313
+ }
314
+ for (const entry of entries) {
315
+ if (isExcludedTopLevelSkillEntry(entry.name) || !entry.isDirectory() && !entry.isSymbolicLink()) {
316
+ continue;
317
+ }
318
+ const candidatePath = path.join(source.root, entry.name);
319
+ let resolvedPath;
320
+ try {
321
+ resolvedPath = entry.isSymbolicLink() ? path.resolve(
322
+ path.dirname(candidatePath),
323
+ await fs.readlink(candidatePath)
324
+ ) : path.resolve(candidatePath);
325
+ } catch {
326
+ continue;
327
+ }
328
+ if (entry.isSymbolicLink() && isManagedTopLevelSymlinkTarget(resolvedPath, paths)) {
329
+ continue;
330
+ }
331
+ if (!await readSkillMdIfValid(resolvedPath)) {
332
+ continue;
333
+ }
334
+ candidates.push({
335
+ name: safeName(entry.name, paths.skillsDir),
336
+ root: source.root,
337
+ label: source.label,
338
+ path: candidatePath,
339
+ resolvedPath,
340
+ priority: source.priority,
341
+ isScoped: false,
342
+ isSymlink: entry.isSymbolicLink()
343
+ });
344
+ }
345
+ }
346
+ return candidates.sort(
347
+ (a, b) => a.priority - b.priority || a.name.localeCompare(b.name)
348
+ );
349
+ }
350
+ async function gatherTopLevelLocalSkills(paths, options = {}) {
351
+ await ensureCanonicalSkillsDir(paths);
352
+ const protectedSkillNames = options.protectedSkillNames || /* @__PURE__ */ new Set();
353
+ const timestamp = options.timestamp || (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
354
+ const backupRoot = backupRootFor(paths, timestamp);
355
+ const candidates = await listTopLevelSkillCandidates(paths, options);
356
+ const byName = /* @__PURE__ */ new Map();
357
+ for (const candidate of candidates) {
358
+ const existing = byName.get(candidate.name) || [];
359
+ existing.push(candidate);
360
+ byName.set(candidate.name, existing);
361
+ }
362
+ let gathered = 0;
363
+ let backedUp = 0;
364
+ let skipped = 0;
365
+ const metadata = {
366
+ version: 1,
367
+ gatheredAt: (/* @__PURE__ */ new Date()).toISOString(),
368
+ skills: {}
369
+ };
370
+ for (const [skillName, skillCandidates] of byName) {
371
+ const protectedFromLocalGather = protectedSkillNames.has(skillName);
372
+ const canonical = protectedFromLocalGather ? skillCandidates.find((candidate) => candidate.isScoped) || null : skillCandidates[0];
373
+ const destinationName = safeName(skillName, paths.skillsDir);
374
+ const destinationDir = path.join(paths.skillsDir, destinationName);
375
+ const skippedSources = [];
376
+ if (!canonical) {
377
+ for (const candidate of skillCandidates) {
378
+ if (candidate.isSymlink) {
379
+ skipped += 1;
380
+ skippedSources.push(candidate.path);
381
+ continue;
382
+ }
383
+ const backupDir = path.join(
384
+ backupRoot,
385
+ relativeBackupPath(candidate.label, skillName)
386
+ );
387
+ try {
388
+ await moveDirectory(candidate.path, backupDir);
389
+ backedUp += 1;
390
+ skippedSources.push(candidate.path);
391
+ } catch {
392
+ skipped += 1;
393
+ }
394
+ }
395
+ metadata.skills[skillName] = {
396
+ canonicalPath: null,
397
+ skippedSources,
398
+ protectedFromLocalGather: true
399
+ };
400
+ continue;
401
+ }
402
+ if (!canonical.isScoped && !await pathExists(destinationDir)) {
403
+ try {
404
+ if (canonical.isSymlink) {
405
+ await createDirectorySymlink(canonical.resolvedPath, destinationDir);
406
+ } else {
407
+ await moveDirectory(canonical.path, destinationDir);
408
+ }
409
+ gathered += 1;
410
+ } catch {
411
+ skipped += 1;
412
+ }
413
+ }
414
+ for (const candidate of skillCandidates) {
415
+ if (candidate === canonical || candidate.isScoped) {
416
+ continue;
417
+ }
418
+ if (candidate.isSymlink) {
419
+ skippedSources.push(candidate.path);
420
+ continue;
421
+ }
422
+ const backupDir = path.join(
423
+ backupRoot,
424
+ relativeBackupPath(candidate.label, skillName)
425
+ );
426
+ try {
427
+ await moveDirectory(candidate.path, backupDir);
428
+ backedUp += 1;
429
+ skippedSources.push(candidate.path);
430
+ } catch {
431
+ skipped += 1;
432
+ }
433
+ }
434
+ metadata.skills[skillName] = {
435
+ canonicalPath: await pathExists(destinationDir) ? destinationDir : canonical.path,
436
+ skippedSources,
437
+ ...protectedFromLocalGather ? { protectedFromLocalGather: true } : {}
438
+ };
439
+ }
440
+ await fs.mkdir(path.dirname(paths.gatherMetadataPath), { recursive: true });
441
+ await fs.writeFile(
442
+ paths.gatherMetadataPath,
443
+ `${JSON.stringify(metadata, null, 2)}
444
+ `,
445
+ "utf8"
446
+ );
447
+ return {
448
+ gathered,
449
+ backedUp,
450
+ skipped,
451
+ metadataPath: paths.gatherMetadataPath
452
+ };
453
+ }
454
+ async function readSkillLock(paths = DEFAULT_SYNC_PATHS) {
455
+ const lockData = await readJsonFile(paths.skillLockPath);
456
+ const sourceUrls = {};
457
+ for (const [skillName, entry] of Object.entries(lockData?.skills || {})) {
458
+ if (typeof entry?.sourceUrl === "string" && entry.sourceUrl.trim()) {
459
+ sourceUrls[skillName] = entry.sourceUrl.trim();
460
+ }
461
+ }
462
+ return sourceUrls;
463
+ }
464
+ async function ensureCanonicalSkillsDir(paths = DEFAULT_SYNC_PATHS) {
465
+ await fs.mkdir(paths.skillsDir, { recursive: true });
466
+ }
467
+ async function scanLocalSkills(paths = DEFAULT_SYNC_PATHS) {
468
+ await ensureCanonicalSkillsDir(paths);
469
+ const sourceUrls = await readSkillLock(paths);
470
+ const entries = await fs.readdir(paths.skillsDir, { withFileTypes: true });
471
+ const skills = await Promise.all(
472
+ entries.map(async (entry) => {
473
+ if (!entry.isDirectory() && !entry.isSymbolicLink() || isTransientSkillDirectoryName(entry.name)) {
474
+ return null;
475
+ }
476
+ const skillDir = path.join(paths.skillsDir, entry.name);
477
+ const skillMdPath = path.join(skillDir, "SKILL.md");
478
+ try {
479
+ const skillMd = await fs.readFile(skillMdPath, "utf8");
480
+ const { description } = parseFrontMatter(skillMd);
481
+ const folderHash = await computeFolderHash(skillDir);
482
+ const skill = {
483
+ name: entry.name,
484
+ skillMd,
485
+ description,
486
+ folderHash,
487
+ directoryPath: skillDir
488
+ };
489
+ if (sourceUrls[entry.name]) {
490
+ skill.sourceUrl = sourceUrls[entry.name];
491
+ }
492
+ return skill;
493
+ } catch {
494
+ return null;
495
+ }
496
+ })
497
+ );
498
+ return skills.filter((skill) => skill !== null).sort((a, b) => a.name.localeCompare(b.name));
499
+ }
500
+ function normalizeSyncState(state) {
501
+ if (!state || state.version !== 1 || typeof state.skills !== "object") {
502
+ return null;
503
+ }
504
+ const normalizeStoredAgentTargets = (targets) => ({
505
+ notis: Boolean(targets?.notis ?? true),
506
+ claude_code: Boolean(targets?.claude_code ?? true),
507
+ cursor: Boolean(targets?.cursor ?? true),
508
+ codex: Boolean(targets?.codex ?? true)
509
+ });
510
+ return {
511
+ version: 1,
512
+ lastSyncedAt: typeof state.lastSyncedAt === "string" ? state.lastSyncedAt : null,
513
+ skills: Object.fromEntries(
514
+ Object.entries(state.skills || {}).map(([skillName, skillState]) => [
515
+ skillName,
516
+ {
517
+ ...skillState,
518
+ agentTargets: normalizeStoredAgentTargets(skillState.agentTargets)
519
+ }
520
+ ])
521
+ )
522
+ };
523
+ }
524
+ async function readSyncState(paths = DEFAULT_SYNC_PATHS) {
525
+ const scopedState = normalizeSyncState(
526
+ await readJsonFile(paths.syncStatePath)
527
+ );
528
+ if (scopedState) {
529
+ return scopedState;
530
+ }
531
+ return DEFAULT_SYNC_STATE;
532
+ }
533
+ async function readLegacySyncState(paths = DEFAULT_SYNC_PATHS) {
534
+ const legacyStatePaths = [
535
+ paths.legacyScopedSyncStatePath,
536
+ path.resolve(paths.syncStatePath) === path.resolve(LEGACY_NOTIS_SYNC_STATE_PATH) ? paths.syncStatePath : path.join(paths.agentsDir, ".notis-sync.json")
537
+ ];
538
+ for (const legacyStatePath of legacyStatePaths) {
539
+ const state = normalizeSyncState(
540
+ await readJsonFile(legacyStatePath)
541
+ );
542
+ if (state) {
543
+ return state;
544
+ }
545
+ }
546
+ return null;
547
+ }
548
+ async function writeSyncState(state, paths = DEFAULT_SYNC_PATHS) {
549
+ await fs.mkdir(path.dirname(paths.syncStatePath), { recursive: true });
550
+ await fs.writeFile(
551
+ paths.syncStatePath,
552
+ `${JSON.stringify(state, null, 2)}
553
+ `,
554
+ "utf8"
555
+ );
556
+ }
557
+ function safeName(name, baseDir = AGENTS_SKILLS_DIR) {
558
+ const sanitizedName = name.trim().replace(/[\\/]+/g, "").replace(/\.\./g, "");
559
+ if (!sanitizedName) {
560
+ throw new Error("Invalid skill name");
561
+ }
562
+ const resolvedBaseDir = path.resolve(baseDir);
563
+ const resolvedPath = path.resolve(resolvedBaseDir, sanitizedName);
564
+ if (!isResolvedChildPath(resolvedBaseDir, resolvedPath)) {
565
+ throw new Error("Skill name resolves outside the expected directory");
566
+ }
567
+ return sanitizedName;
568
+ }
569
+ async function deleteLocalSkill(skillName, paths = DEFAULT_SYNC_PATHS) {
570
+ const skillDir = path.join(
571
+ paths.skillsDir,
572
+ safeName(skillName, paths.skillsDir)
573
+ );
574
+ try {
575
+ await fs.rm(skillDir, { recursive: true, force: true });
576
+ return true;
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+ async function createZipFromDirectory(directoryPath) {
582
+ const zipPath = path.join(
583
+ os.tmpdir(),
584
+ `notis-skill-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
585
+ );
586
+ const parentDir = path.dirname(directoryPath);
587
+ const directoryName = path.basename(directoryPath);
588
+ if (process.platform === "win32") {
589
+ await execFileAsync("powershell.exe", [
590
+ "-NoProfile",
591
+ "-Command",
592
+ `Compress-Archive -Path '${directoryPath.replace(/'/g, "''")}' -DestinationPath '${zipPath.replace(/'/g, "''")}' -Force`
593
+ ]);
594
+ return zipPath;
595
+ }
596
+ await execFileAsync("zip", ["-qry", zipPath, directoryName], {
597
+ cwd: parentDir
598
+ });
599
+ return zipPath;
600
+ }
601
+ async function extractZipToDirectory(zipPath, destinationDir) {
602
+ const extractRoot = await fs.mkdtemp(
603
+ path.join(os.tmpdir(), "notis-skill-extract-")
604
+ );
605
+ try {
606
+ if (process.platform === "win32") {
607
+ await execFileAsync("powershell.exe", [
608
+ "-NoProfile",
609
+ "-Command",
610
+ `Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${extractRoot.replace(/'/g, "''")}' -Force`
611
+ ]);
612
+ } else {
613
+ await execFileAsync("unzip", ["-qq", zipPath, "-d", extractRoot]);
614
+ }
615
+ const extractedEntries = await fs.readdir(extractRoot, {
616
+ withFileTypes: true
617
+ });
618
+ const extractedDirectory = extractedEntries.find(
619
+ (entry) => entry.isDirectory()
620
+ );
621
+ const sourceDir = extractedDirectory ? path.join(extractRoot, extractedDirectory.name) : extractRoot;
622
+ await fs.rm(destinationDir, { recursive: true, force: true });
623
+ await fs.mkdir(path.dirname(destinationDir), { recursive: true });
624
+ await fs.cp(sourceDir, destinationDir, { recursive: true });
625
+ } finally {
626
+ await fs.rm(extractRoot, { recursive: true, force: true });
627
+ }
628
+ }
629
+ async function pathExists(targetPath) {
630
+ try {
631
+ await fs.access(targetPath);
632
+ return true;
633
+ } catch {
634
+ return false;
635
+ }
636
+ }
637
+ async function replaceSkillDirectoryAtomically(skillDir, populateDir) {
638
+ const parentDir = path.dirname(skillDir);
639
+ const skillName = path.basename(skillDir);
640
+ await fs.mkdir(parentDir, { recursive: true });
641
+ const stagingDir = await fs.mkdtemp(
642
+ path.join(parentDir, `${skillName}.staging-`)
643
+ );
644
+ const backupDir = path.join(
645
+ parentDir,
646
+ `${skillName}.backup-${Date.now()}-${Math.random().toString(36).slice(2)}`
647
+ );
648
+ let movedExisting = false;
649
+ let promotedStaging = false;
650
+ let cleanupError = null;
651
+ try {
652
+ await populateDir(stagingDir);
653
+ if (await pathExists(skillDir)) {
654
+ await fs.rename(skillDir, backupDir);
655
+ movedExisting = true;
656
+ }
657
+ await fs.rename(stagingDir, skillDir);
658
+ promotedStaging = true;
659
+ } catch (error) {
660
+ if (movedExisting && !promotedStaging && await pathExists(backupDir)) {
661
+ await fs.rename(backupDir, skillDir);
662
+ }
663
+ throw error;
664
+ } finally {
665
+ if (!promotedStaging && await pathExists(stagingDir)) {
666
+ await fs.rm(stagingDir, { recursive: true, force: true });
667
+ }
668
+ if ((promotedStaging || !movedExisting) && await pathExists(backupDir)) {
669
+ try {
670
+ await fs.rm(backupDir, { recursive: true, force: true });
671
+ } catch (error) {
672
+ if (!cleanupError) {
673
+ cleanupError = error instanceof Error ? error : new Error(String(error));
674
+ }
675
+ }
676
+ }
677
+ if (cleanupError) {
678
+ console.warn(
679
+ `[Notis] Failed to clean up temporary skill directory backup for "${skillDir}"`,
680
+ cleanupError
681
+ );
682
+ }
683
+ }
684
+ }
685
+ function bundleFilesIncludeSkillMd(bundleFiles) {
686
+ return bundleFiles.some((bundleFile) => {
687
+ const basename = path.basename(bundleFile.path).toLowerCase();
688
+ return basename === "skill.md" || basename === "skills.md";
689
+ });
690
+ }
691
+ async function createSkillBundleBase64(skill) {
692
+ const zipPath = await createZipFromDirectory(skill.directoryPath);
693
+ try {
694
+ const zipBytes = await fs.readFile(zipPath);
695
+ return zipBytes.toString("base64");
696
+ } finally {
697
+ await fs.rm(zipPath, { force: true });
698
+ }
699
+ }
700
+ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PATHS) {
701
+ const skillDir = path.join(
702
+ paths.skillsDir,
703
+ safeName(skill.name, paths.skillsDir)
704
+ );
705
+ if (bundleBytes?.length) {
706
+ const bundlePath = path.join(
707
+ os.tmpdir(),
708
+ `notis-skill-download-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
709
+ );
710
+ try {
711
+ await fs.writeFile(bundlePath, bundleBytes);
712
+ await extractZipToDirectory(bundlePath, skillDir);
713
+ return true;
714
+ } finally {
715
+ await fs.rm(bundlePath, { force: true });
716
+ }
717
+ }
718
+ if (!skill.skill_md && (!skill.bundle_files || skill.bundle_files.length === 0)) {
719
+ return false;
720
+ }
721
+ if (skill.bundle_files && skill.bundle_files.length > 0) {
722
+ if (!bundleFilesIncludeSkillMd(skill.bundle_files)) {
723
+ throw new Error(
724
+ `Synced bundle for "${skill.name}" is missing SKILL.md or SKILLS.md`
725
+ );
726
+ }
727
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
728
+ for (const bundleFile of skill.bundle_files || []) {
729
+ const filePath = resolveSkillBundleFilePath(
730
+ stagingDir,
731
+ bundleFile.path
732
+ );
733
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
734
+ await fs.writeFile(
735
+ filePath,
736
+ Buffer.from(bundleFile.content_b64, "base64")
737
+ );
738
+ }
739
+ });
740
+ return true;
741
+ }
742
+ await replaceSkillDirectoryAtomically(skillDir, async (stagingDir) => {
743
+ await fs.writeFile(
744
+ path.join(stagingDir, "SKILL.md"),
745
+ skill.skill_md ?? "",
746
+ "utf8"
747
+ );
748
+ });
749
+ return true;
750
+ }
751
+
752
+ // src/runtime/skill-sync/cloud-client.ts
753
+ async function requestJson(url, jwt, options = {}) {
754
+ const response = await fetch(url, {
755
+ method: options.method || "POST",
756
+ headers: {
757
+ "Content-Type": "application/json",
758
+ Authorization: `Bearer ${jwt}`
759
+ },
760
+ body: options.body ? JSON.stringify(options.body) : void 0
761
+ });
762
+ if (!response.ok) {
763
+ const text = await response.text();
764
+ throw new Error(`${options.method || "POST"} ${url} \u2192 ${response.status}: ${text}`);
765
+ }
766
+ return response.json();
767
+ }
768
+ async function fetchSyncSettings(serverUrl, jwt) {
769
+ return requestJson(`${serverUrl}/portal_skills/sync-settings`, jwt, {
770
+ body: {}
771
+ });
772
+ }
773
+ async function pullSkills(serverUrl, jwt) {
774
+ return requestJson(`${serverUrl}/portal_skills/sync-pull`, jwt, {
775
+ body: {}
776
+ });
777
+ }
778
+ async function pushChangedSkills(serverUrl, jwt, changedSkills) {
779
+ const payloadSkills = await Promise.all(changedSkills.map(async (skill) => ({
780
+ name: skill.name,
781
+ description: skill.description,
782
+ skill_md: skill.skillMd,
783
+ source_url: skill.sourceUrl,
784
+ folder_hash: skill.folderHash,
785
+ bundle_base64: await createSkillBundleBase64(skill)
786
+ })));
787
+ return requestJson(`${serverUrl}/portal_skills/sync-push`, jwt, {
788
+ body: {
789
+ skills: payloadSkills
790
+ }
791
+ });
792
+ }
793
+ async function downloadSkillBundle(bundleUrl) {
794
+ const response = await fetch(bundleUrl);
795
+ if (!response.ok) {
796
+ const text = await response.text();
797
+ throw new Error(`GET ${bundleUrl} \u2192 ${response.status}: ${text}`);
798
+ }
799
+ return Buffer.from(await response.arrayBuffer());
800
+ }
801
+ async function updateAgentTargets(serverUrl, jwt, skillId, targets) {
802
+ return requestJson(`${serverUrl}/portal_skills/agent-targets`, jwt, {
803
+ method: "PATCH",
804
+ body: {
805
+ skill_id: skillId,
806
+ agent_targets: targets
807
+ }
808
+ });
809
+ }
810
+
811
+ // src/runtime/skill-sync/symlink-manager.ts
812
+ import { promises as fs2 } from "fs";
813
+ import os2 from "os";
814
+ import path2 from "path";
815
+ var HOME_DIR2 = os2.homedir();
816
+ var EXTERNAL_AGENT_SKILL_DIRS = {
817
+ claude_code: path2.join(HOME_DIR2, ".claude", "skills"),
818
+ cursor: path2.join(HOME_DIR2, ".cursor", "skills"),
819
+ codex: path2.join(HOME_DIR2, ".codex", "skills")
820
+ };
821
+ var EXTERNAL_AGENTS = Object.keys(EXTERNAL_AGENT_SKILL_DIRS);
822
+ async function removeForeignAccountSymlinks(skillsDir, options = {}) {
823
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
824
+ const currentRoot = path2.resolve(skillsDir);
825
+ const foreignCapableRoots = [
826
+ path2.join(path2.resolve(NOTIS_SKILL_SYNC_ROOT), "users"),
827
+ path2.join(path2.resolve(AGENTS_DIR), "notis", "users"),
828
+ path2.dirname(path2.dirname(currentRoot))
829
+ ];
830
+ let removed = 0;
831
+ const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
832
+ for (const agentDir of /* @__PURE__ */ new Set([...Object.values(agentSkillDirs), legacyGlobalSkillsDir])) {
833
+ let entries;
834
+ try {
835
+ entries = await fs2.readdir(agentDir, { withFileTypes: true });
836
+ } catch (error) {
837
+ if (error?.code === "ENOENT") continue;
838
+ throw error;
839
+ }
840
+ for (const entry of entries) {
841
+ if (!entry.isSymbolicLink()) continue;
842
+ const entryPath = path2.join(agentDir, entry.name);
843
+ try {
844
+ const target = await fs2.readlink(entryPath);
845
+ const resolvedTarget = path2.resolve(path2.dirname(entryPath), target);
846
+ const belongsToAnyAccount = foreignCapableRoots.some((root) => resolvedTarget.startsWith(`${root}${path2.sep}`));
847
+ const belongsToCurrentAccount = resolvedTarget === currentRoot || resolvedTarget.startsWith(`${currentRoot}${path2.sep}`);
848
+ if (belongsToAnyAccount && !belongsToCurrentAccount) {
849
+ await fs2.unlink(entryPath);
850
+ removed += 1;
851
+ }
852
+ } catch (error) {
853
+ if (error?.code !== "ENOENT") throw error;
854
+ }
855
+ }
856
+ }
857
+ return removed;
858
+ }
859
+ function managedSkillRoots(skillsDir, legacyGlobalSkillsDir = LEGACY_AGENTS_SKILLS_DIR) {
860
+ const resolvedSkillsDir = path2.resolve(skillsDir);
861
+ const scopedUsersRoot = path2.dirname(path2.dirname(resolvedSkillsDir));
862
+ const roots = [
863
+ resolvedSkillsDir,
864
+ path2.resolve(legacyGlobalSkillsDir),
865
+ path2.join(path2.resolve(NOTIS_SKILL_SYNC_ROOT), "users"),
866
+ path2.join(path2.resolve(AGENTS_DIR), "notis", "users")
867
+ ];
868
+ if (path2.basename(scopedUsersRoot) === "users") {
869
+ roots.push(scopedUsersRoot);
870
+ }
871
+ return roots;
872
+ }
873
+ async function isManagedSymlink(linkPath, managedRoots) {
874
+ try {
875
+ const stats = await fs2.lstat(linkPath);
876
+ if (!stats.isSymbolicLink()) {
877
+ return false;
878
+ }
879
+ const target = await fs2.readlink(linkPath);
880
+ const resolvedTarget = path2.resolve(path2.dirname(linkPath), target);
881
+ return managedRoots.some((root) => resolvedTarget === root || resolvedTarget.startsWith(`${root}${path2.sep}`));
882
+ } catch {
883
+ return false;
884
+ }
885
+ }
886
+ async function ensureCorrectSymlink(linkPath, targetPath) {
887
+ try {
888
+ const stats = await fs2.lstat(linkPath);
889
+ if (stats.isSymbolicLink()) {
890
+ const currentTarget = await fs2.readlink(linkPath);
891
+ const resolvedTarget = path2.resolve(path2.dirname(linkPath), currentTarget);
892
+ if (resolvedTarget === targetPath) {
893
+ return "skipped";
894
+ }
895
+ await fs2.unlink(linkPath);
896
+ } else {
897
+ return "blocked";
898
+ }
899
+ } catch {
900
+ }
901
+ const relativePath = path2.relative(path2.dirname(linkPath), targetPath);
902
+ await fs2.symlink(relativePath, linkPath);
903
+ return "linked";
904
+ }
905
+ function defaultAgentSkillDirs(skillsDir) {
906
+ return {
907
+ notis: skillsDir,
908
+ ...EXTERNAL_AGENT_SKILL_DIRS
909
+ };
910
+ }
911
+ async function removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots) {
912
+ let removed = 0;
913
+ try {
914
+ await fs2.mkdir(agentDir, { recursive: true });
915
+ const existingEntries = await fs2.readdir(agentDir, { withFileTypes: true });
916
+ for (const entry of existingEntries) {
917
+ const entryPath = path2.join(agentDir, entry.name);
918
+ if (!desiredSkills.has(entry.name) && await isManagedSymlink(entryPath, managedRoots)) {
919
+ await fs2.unlink(entryPath);
920
+ removed += 1;
921
+ }
922
+ }
923
+ } catch {
924
+ }
925
+ return removed;
926
+ }
927
+ async function removeAllSymlinksForSkill(skillName, skillsDir = LEGACY_AGENTS_SKILLS_DIR, options = {}) {
928
+ let removed = 0;
929
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
930
+ const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
931
+ const managedRoots = managedSkillRoots(skillsDir, legacyGlobalSkillsDir);
932
+ const agentDirs = /* @__PURE__ */ new Set([...Object.values(agentSkillDirs), legacyGlobalSkillsDir]);
933
+ const expectedTarget = path2.resolve(skillsDir, safeName(skillName, skillsDir));
934
+ for (const agentDir of agentDirs) {
935
+ const linkPath = path2.join(agentDir, safeName(skillName, agentDir));
936
+ const owned = options.ownership === "exact-skill-dir" ? await isManagedSymlink(linkPath, [expectedTarget]) : await isManagedSymlink(linkPath, managedRoots);
937
+ if (owned) {
938
+ await fs2.unlink(linkPath);
939
+ removed += 1;
940
+ }
941
+ }
942
+ return removed;
943
+ }
944
+ async function detectDeletedAgentSymlinks(cloudSkills, previousState, skillsDir = LEGACY_AGENTS_SKILLS_DIR, options = {}) {
945
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
946
+ const deletions = [];
947
+ for (const agent of EXTERNAL_AGENTS) {
948
+ const agentDir = agentSkillDirs[agent];
949
+ if (path2.resolve(agentDir) === path2.resolve(skillsDir)) {
950
+ continue;
951
+ }
952
+ try {
953
+ const dirStat = await fs2.stat(agentDir);
954
+ if (!dirStat.isDirectory()) {
955
+ continue;
956
+ }
957
+ } catch {
958
+ continue;
959
+ }
960
+ for (const skill of cloudSkills) {
961
+ if (skill.status !== "active") {
962
+ continue;
963
+ }
964
+ if (!skill.id || skill.id.startsWith("local-")) {
965
+ continue;
966
+ }
967
+ const previous = previousState.skills[skill.name];
968
+ if (!previous) {
969
+ continue;
970
+ }
971
+ const cloudTargets = normalizeAgentTargets(skill.agent_targets);
972
+ const previousTargets = normalizeAgentTargets(previous.agentTargets);
973
+ if (!cloudTargets[agent] || !previousTargets[agent]) {
974
+ continue;
975
+ }
976
+ let safeSkillName;
977
+ try {
978
+ safeSkillName = safeName(skill.name, skillsDir);
979
+ } catch {
980
+ continue;
981
+ }
982
+ const linkPath = path2.join(agentDir, safeName(safeSkillName, agentDir));
983
+ try {
984
+ await fs2.lstat(linkPath);
985
+ } catch (error) {
986
+ if (error?.code === "ENOENT") {
987
+ deletions.push({ skillId: skill.id, skillName: skill.name, agent });
988
+ }
989
+ }
990
+ }
991
+ }
992
+ return deletions;
993
+ }
994
+ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, options = {}) {
995
+ await fs2.mkdir(skillsDir, { recursive: true });
996
+ const result = {
997
+ linked: 0,
998
+ removed: 0,
999
+ skipped: 0
1000
+ };
1001
+ const agentSkillDirs = { ...defaultAgentSkillDirs(skillsDir), ...options.agentSkillDirs };
1002
+ const legacyGlobalSkillsDir = options.legacyGlobalSkillsDir || LEGACY_AGENTS_SKILLS_DIR;
1003
+ const managedRoots = managedSkillRoots(skillsDir, legacyGlobalSkillsDir);
1004
+ const desiredByAgent = {
1005
+ notis: /* @__PURE__ */ new Set(),
1006
+ claude_code: /* @__PURE__ */ new Set(),
1007
+ cursor: /* @__PURE__ */ new Set(),
1008
+ codex: /* @__PURE__ */ new Set()
1009
+ };
1010
+ for (const skill of skills) {
1011
+ if (skill.status !== "active") {
1012
+ continue;
1013
+ }
1014
+ const safeSkillName = safeName(skill.name, skillsDir);
1015
+ const targets = normalizeAgentTargets(skill.agent_targets);
1016
+ if (targets.notis) {
1017
+ desiredByAgent.notis.add(safeSkillName);
1018
+ }
1019
+ if (targets.claude_code) {
1020
+ desiredByAgent.claude_code.add(safeSkillName);
1021
+ }
1022
+ if (targets.cursor) {
1023
+ desiredByAgent.cursor.add(safeSkillName);
1024
+ }
1025
+ if (targets.codex) {
1026
+ desiredByAgent.codex.add(safeSkillName);
1027
+ }
1028
+ }
1029
+ for (const [agent, agentDir] of Object.entries(agentSkillDirs)) {
1030
+ if (path2.resolve(agentDir) === path2.resolve(skillsDir)) {
1031
+ result.skipped += desiredByAgent[agent].size;
1032
+ continue;
1033
+ }
1034
+ await fs2.mkdir(agentDir, { recursive: true });
1035
+ const desiredSkills = desiredByAgent[agent];
1036
+ if (options.removeUndesired !== false) {
1037
+ result.removed += await removeUndesiredManagedSymlinks(agentDir, desiredSkills, managedRoots);
1038
+ }
1039
+ for (const skillName of desiredSkills) {
1040
+ const targetPath = path2.join(skillsDir, skillName);
1041
+ const linkPath = path2.join(agentDir, safeName(skillName, agentDir));
1042
+ try {
1043
+ await fs2.access(targetPath);
1044
+ } catch {
1045
+ result.skipped += 1;
1046
+ continue;
1047
+ }
1048
+ const syncOutcome = await ensureCorrectSymlink(linkPath, targetPath);
1049
+ if (syncOutcome === "linked") {
1050
+ result.linked += 1;
1051
+ } else if (syncOutcome === "blocked") {
1052
+ console.warn(
1053
+ `[skill-sync] Could not link "${skillName}" for ${agent}: non-symlink entry blocks ${linkPath}`
1054
+ );
1055
+ result.skipped += 1;
1056
+ } else {
1057
+ result.skipped += 1;
1058
+ }
1059
+ }
1060
+ }
1061
+ if (options.removeUndesired !== false && !Object.values(agentSkillDirs).some(
1062
+ (agentDir) => path2.resolve(agentDir) === path2.resolve(legacyGlobalSkillsDir)
1063
+ )) {
1064
+ result.removed += await removeUndesiredManagedSymlinks(
1065
+ legacyGlobalSkillsDir,
1066
+ /* @__PURE__ */ new Set(),
1067
+ managedRoots
1068
+ );
1069
+ }
1070
+ return result;
1071
+ }
1072
+
1073
+ // src/runtime/skill-sync/sync-plan.ts
1074
+ function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames) {
1075
+ return localSkills.filter((skill) => {
1076
+ if (cloudCuratedSkillNames.has(skill.name)) {
1077
+ return false;
1078
+ }
1079
+ const previous = syncState.skills[skill.name];
1080
+ if (!previous) {
1081
+ return true;
1082
+ }
1083
+ if (cloudSkillNames && !cloudSkillNames.has(skill.name)) {
1084
+ return false;
1085
+ }
1086
+ return previous.folderHash !== skill.folderHash;
1087
+ });
1088
+ }
1089
+
1090
+ // src/runtime/skill-sync/write-cloud-skill.ts
1091
+ async function writeCloudSkillWithBundleFallback(skill, dependencies) {
1092
+ if (skill.skill_source_url) {
1093
+ try {
1094
+ const bundleBytes = await dependencies.downloadSkillBundle(skill.skill_source_url);
1095
+ const wroteBundleToDisk = await dependencies.writeCloudSkillToDisk(skill, bundleBytes);
1096
+ if (wroteBundleToDisk) {
1097
+ return true;
1098
+ }
1099
+ dependencies.onWarning?.(
1100
+ `Bundle sync for "${skill.name}" produced no local changes, falling back to SKILL.md payload.`,
1101
+ new Error("Bundle write returned false")
1102
+ );
1103
+ } catch (error) {
1104
+ dependencies.onWarning?.(
1105
+ `Failed to apply bundle sync for "${skill.name}", falling back to SKILL.md payload.`,
1106
+ error
1107
+ );
1108
+ }
1109
+ }
1110
+ if (skill.bundle_hydration_failed) {
1111
+ dependencies.onWarning?.(
1112
+ `Skipping markdown fallback for synced skill "${skill.name}" because its stored bundle could not be hydrated by the server.`,
1113
+ new Error("Bundle hydration failed")
1114
+ );
1115
+ return false;
1116
+ }
1117
+ try {
1118
+ return await dependencies.writeCloudSkillToDisk(skill);
1119
+ } catch (error) {
1120
+ dependencies.onWarning?.(
1121
+ `Failed to write synced skill "${skill.name}" to disk.`,
1122
+ error
1123
+ );
1124
+ return false;
1125
+ }
1126
+ }
1127
+
1128
+ // src/runtime/skill-sync/index.ts
1129
+ var BASE_SKILL_NAMES = /* @__PURE__ */ new Set(["notis-apps", "notis-query", "notis-cli"]);
1130
+ function withoutBaseSkills(pullResponse) {
1131
+ return {
1132
+ ...pullResponse,
1133
+ skills: pullResponse.skills.filter((skill) => !BASE_SKILL_NAMES.has(skill.name))
1134
+ };
1135
+ }
1136
+ function withoutBaseSkillState(state) {
1137
+ return {
1138
+ ...state,
1139
+ skills: Object.fromEntries(
1140
+ Object.entries(state.skills).filter(([name]) => !BASE_SKILL_NAMES.has(name))
1141
+ )
1142
+ };
1143
+ }
1144
+ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
1145
+ fetchSyncSettings,
1146
+ pullSkills,
1147
+ pushChangedSkills,
1148
+ downloadSkillBundle,
1149
+ gatherTopLevelLocalSkills,
1150
+ readLegacySyncState,
1151
+ readSyncState,
1152
+ scanLocalSkills,
1153
+ deleteLocalSkill,
1154
+ writeCloudSkillToDisk,
1155
+ writeSyncState,
1156
+ removeAllSymlinksForSkill,
1157
+ syncSymlinks,
1158
+ detectDeletedAgentSymlinks,
1159
+ removeForeignAccountSymlinks,
1160
+ updateAgentTargets
1161
+ };
1162
+ function toSkillMap(skills) {
1163
+ return new Map(skills.map((skill) => [skill.name, skill]));
1164
+ }
1165
+ function decodeJwtSubject(jwt) {
1166
+ try {
1167
+ const parts = jwt.split(".");
1168
+ if (parts.length !== 3) return null;
1169
+ const decoded = JSON.parse(
1170
+ Buffer.from(parts[1], "base64url").toString()
1171
+ );
1172
+ return typeof decoded.sub === "string" && decoded.sub.trim() ? decoded.sub.trim() : null;
1173
+ } catch {
1174
+ return null;
1175
+ }
1176
+ }
1177
+ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
1178
+ const skillName = cloudSkill.name;
1179
+ const cloudHash = cloudSkill.skill_folder_hash || "";
1180
+ const localSkill = localSkills.get(skillName);
1181
+ if (!localSkill) {
1182
+ return true;
1183
+ }
1184
+ if (cloudSkill.source === "curated") {
1185
+ return cloudHash ? cloudHash !== localSkill.folderHash : true;
1186
+ }
1187
+ const previous = previousState.skills[skillName];
1188
+ const localChangedSinceLastSync = !previous || previous.folderHash !== localSkill.folderHash;
1189
+ return !localChangedSinceLastSync && Boolean(cloudHash) && cloudHash !== localSkill.folderHash;
1190
+ }
1191
+ function buildSyncState(pullResponse, localSkills, lastSyncedAt) {
1192
+ const localSkillMap = toSkillMap(localSkills);
1193
+ const skills = Object.fromEntries(
1194
+ pullResponse.skills.map((skill) => {
1195
+ const localSkill = localSkillMap.get(skill.name);
1196
+ return [
1197
+ skill.name,
1198
+ {
1199
+ cloudId: skill.id,
1200
+ folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
1201
+ agentTargets: normalizeAgentTargets(skill.agent_targets),
1202
+ syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
1203
+ }
1204
+ ];
1205
+ })
1206
+ );
1207
+ return {
1208
+ version: 1,
1209
+ lastSyncedAt,
1210
+ skills
1211
+ };
1212
+ }
1213
+ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
1214
+ const cloudSkillNames = new Set(
1215
+ pullResponse.skills.map((skill) => skill.name)
1216
+ );
1217
+ const localOnlySkills = localSkills.filter((skill) => !cloudSkillNames.has(skill.name)).map((skill) => {
1218
+ const previous = previousState.skills[skill.name];
1219
+ return {
1220
+ id: previous?.cloudId || `local-${skill.name}`,
1221
+ name: skill.name,
1222
+ description: skill.description || null,
1223
+ skill_md: skill.skillMd,
1224
+ agent_targets: previous?.agentTargets,
1225
+ skill_folder_hash: skill.folderHash,
1226
+ source: "local",
1227
+ status: "active"
1228
+ };
1229
+ });
1230
+ return [...pullResponse.skills, ...localOnlySkills];
1231
+ }
1232
+ function isEmptySyncState(state) {
1233
+ return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
1234
+ }
1235
+ function applyLegacyFirstRunState(localSkills, scopedState, legacyState) {
1236
+ if (!isEmptySyncState(scopedState) || !legacyState) {
1237
+ return scopedState;
1238
+ }
1239
+ const migratedSkills = Object.fromEntries(
1240
+ localSkills.flatMap((skill) => {
1241
+ const previous = legacyState.skills[skill.name];
1242
+ if (!previous || previous.folderHash !== skill.folderHash) {
1243
+ return [];
1244
+ }
1245
+ return [[skill.name, previous]];
1246
+ })
1247
+ );
1248
+ if (Object.keys(migratedSkills).length === 0) {
1249
+ return scopedState;
1250
+ }
1251
+ return {
1252
+ version: 1,
1253
+ lastSyncedAt: legacyState.lastSyncedAt,
1254
+ skills: migratedSkills
1255
+ };
1256
+ }
1257
+ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previousState, syncPaths, deps) {
1258
+ const localSkillMap = toSkillMap(localSkills);
1259
+ const warnSkillSync = (message, error) => {
1260
+ console.warn(`[Notis] ${message}`, error);
1261
+ };
1262
+ let downloaded = 0;
1263
+ for (const cloudSkill of pullResponse.skills) {
1264
+ if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
1265
+ continue;
1266
+ }
1267
+ if (await writeCloudSkillWithBundleFallback(cloudSkill, {
1268
+ downloadSkillBundle: deps.downloadSkillBundle,
1269
+ writeCloudSkillToDisk: (skill, bundleBytes) => deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths),
1270
+ onWarning: warnSkillSync
1271
+ })) {
1272
+ downloaded += 1;
1273
+ }
1274
+ }
1275
+ return downloaded;
1276
+ }
1277
+ function assertSkillsPullAuthorized(pullResponse) {
1278
+ if (pullResponse.entitlement_access?.code === "entitlement_upgrade_required" && pullResponse.entitlement_access.entitlement === "skills") {
1279
+ throw new Error(
1280
+ "Skill sync access was denied; preserving existing local skills."
1281
+ );
1282
+ }
1283
+ }
1284
+ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies = {}, options = {}) {
1285
+ const deps = {
1286
+ ...DEFAULT_RUN_SKILL_SYNC_DEPS,
1287
+ ...dependencies
1288
+ };
1289
+ const authUserId = decodeJwtSubject(jwt);
1290
+ if (!authUserId) {
1291
+ throw new Error(
1292
+ "Cannot materialize skills without a valid authenticated desktop session."
1293
+ );
1294
+ }
1295
+ const syncPaths = getSkillSyncPathsForUser(authUserId);
1296
+ const pullResponse = await deps.pullSkills(serverUrl, jwt);
1297
+ assertSkillsPullAuthorized(pullResponse);
1298
+ const previousState = await deps.readSyncState(syncPaths);
1299
+ const localSkills = await deps.scanLocalSkills(syncPaths);
1300
+ const downloaded = await writePulledSkillsToScopedMirror(
1301
+ pullResponse,
1302
+ localSkills,
1303
+ previousState,
1304
+ syncPaths,
1305
+ deps
1306
+ );
1307
+ const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
1308
+ const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
1309
+ const relinkSkillNames = new Set(options.relinkSkillNames || []);
1310
+ if (relinkSkillNames.size > 0) {
1311
+ await deps.syncSymlinks(
1312
+ pullResponse.skills.filter((skill) => relinkSkillNames.has(skill.name)),
1313
+ syncPaths.skillsDir,
1314
+ { removeUndesired: false }
1315
+ );
1316
+ }
1317
+ await deps.writeSyncState(
1318
+ buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
1319
+ syncPaths
1320
+ );
1321
+ return {
1322
+ pulled: pullResponse.skills.length,
1323
+ downloaded,
1324
+ deleted: 0,
1325
+ removed: 0,
1326
+ lastSyncedAt
1327
+ };
1328
+ }
1329
+ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previousState, scopedState, skillsDir, deps) {
1330
+ if (isEmptySyncState(scopedState)) {
1331
+ return 0;
1332
+ }
1333
+ const deletions = await deps.detectDeletedAgentSymlinks(
1334
+ pullResponse.skills,
1335
+ previousState,
1336
+ skillsDir
1337
+ );
1338
+ if (deletions.length === 0) {
1339
+ return 0;
1340
+ }
1341
+ const latestSkillsById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
1342
+ let fresh = null;
1343
+ try {
1344
+ fresh = await deps.pullSkills(serverUrl, jwt);
1345
+ } catch (error) {
1346
+ console.warn(
1347
+ "[skill-sync] Could not re-pull latest agent targets before deactivation; using the top-of-sync snapshot.",
1348
+ error
1349
+ );
1350
+ }
1351
+ if (fresh) {
1352
+ assertSkillsPullAuthorized(fresh);
1353
+ for (const skill of fresh.skills) {
1354
+ latestSkillsById.set(skill.id, skill);
1355
+ }
1356
+ }
1357
+ const agentsBySkill = /* @__PURE__ */ new Map();
1358
+ for (const deletion of deletions) {
1359
+ const entry = agentsBySkill.get(deletion.skillId) ?? {
1360
+ skillName: deletion.skillName,
1361
+ agents: /* @__PURE__ */ new Set()
1362
+ };
1363
+ entry.agents.add(deletion.agent);
1364
+ agentsBySkill.set(deletion.skillId, entry);
1365
+ }
1366
+ const inMemoryById = new Map(pullResponse.skills.map((skill) => [skill.id, skill]));
1367
+ let deactivated = 0;
1368
+ for (const [skillId, { skillName, agents }] of agentsBySkill) {
1369
+ const latest = latestSkillsById.get(skillId);
1370
+ if (!latest) {
1371
+ continue;
1372
+ }
1373
+ const nextTargets = { ...normalizeAgentTargets(latest.agent_targets) };
1374
+ for (const agent of agents) {
1375
+ nextTargets[agent] = false;
1376
+ }
1377
+ try {
1378
+ await deps.updateAgentTargets(serverUrl, jwt, skillId, nextTargets);
1379
+ const inMemory = inMemoryById.get(skillId);
1380
+ if (inMemory) {
1381
+ inMemory.agent_targets = nextTargets;
1382
+ }
1383
+ deactivated += agents.size;
1384
+ } catch (error) {
1385
+ console.warn(
1386
+ `[skill-sync] Failed to deactivate "${skillName}" for ${[...agents].join(", ")} after local symlink deletion:`,
1387
+ error
1388
+ );
1389
+ }
1390
+ }
1391
+ return deactivated;
1392
+ }
1393
+ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
1394
+ const deps = {
1395
+ ...DEFAULT_RUN_SKILL_SYNC_DEPS,
1396
+ ...dependencies
1397
+ };
1398
+ const syncSettings = await deps.fetchSyncSettings(
1399
+ serverUrl,
1400
+ jwt
1401
+ );
1402
+ const syncUserId = syncSettings.user_id?.trim() || decodeJwtSubject(jwt);
1403
+ if (!syncUserId) {
1404
+ throw new Error(
1405
+ "Cannot sync skills without a server-verified account identity."
1406
+ );
1407
+ }
1408
+ const syncPaths = getSkillSyncPathsForUser(syncUserId);
1409
+ const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
1410
+ syncPaths.skillsDir
1411
+ );
1412
+ if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
1413
+ return {
1414
+ syncEnabled: false,
1415
+ pushed: 0,
1416
+ pulled: 0,
1417
+ downloaded: 0,
1418
+ deleted: 0,
1419
+ deactivated: 0,
1420
+ linked: 0,
1421
+ removed: foreignLinksRemoved,
1422
+ skipped: 0,
1423
+ lastSyncedAt: syncSettings.last_synced_at,
1424
+ failedPushes: []
1425
+ };
1426
+ }
1427
+ let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1428
+ assertSkillsPullAuthorized(pullResponse);
1429
+ const cloudCuratedSkillNames = new Set(
1430
+ pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
1431
+ );
1432
+ const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
1433
+ const authUserId = decodeJwtSubject(jwt);
1434
+ let previousAuthState = null;
1435
+ if (authUserId && authUserId !== syncUserId) {
1436
+ const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
1437
+ previousAuthState = await deps.readSyncState(previousAuthPaths);
1438
+ await deps.gatherTopLevelLocalSkills(syncPaths, {
1439
+ sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
1440
+ protectedSkillNames
1441
+ });
1442
+ }
1443
+ await deps.gatherTopLevelLocalSkills(syncPaths, {
1444
+ protectedSkillNames
1445
+ });
1446
+ const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1447
+ const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
1448
+ const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
1449
+ localSkills,
1450
+ scopedState,
1451
+ isEmptySyncState(scopedState) ? !previousAuthState || isEmptySyncState(previousAuthState) ? await deps.readLegacySyncState(syncPaths) : previousAuthState : null
1452
+ ));
1453
+ const deactivated = await deactivateDeletedAgentSkills(
1454
+ serverUrl,
1455
+ jwt,
1456
+ pullResponse,
1457
+ previousState,
1458
+ scopedState,
1459
+ syncPaths.skillsDir,
1460
+ deps
1461
+ );
1462
+ await deps.syncSymlinks(
1463
+ buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
1464
+ syncPaths.skillsDir
1465
+ );
1466
+ const pushCandidates = getPushCandidates(
1467
+ localSkills,
1468
+ previousState,
1469
+ cloudCuratedSkillNames,
1470
+ new Set(pullResponse.skills.map((skill) => skill.name))
1471
+ );
1472
+ const failedPushes = [];
1473
+ if (pushCandidates.length > 0) {
1474
+ const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
1475
+ if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
1476
+ failedPushes.push(...pushResult.failed);
1477
+ console.warn(
1478
+ `[skill-sync] ${pushResult.failed.length} skill(s) were rejected during push: ` + pushResult.failed.map((f) => `${f.name} (${f.error})`).join("; ")
1479
+ );
1480
+ }
1481
+ pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
1482
+ assertSkillsPullAuthorized(pullResponse);
1483
+ }
1484
+ const cloudSkillNames = new Set(pullResponse.skills.map((s) => s.name));
1485
+ let deleted = 0;
1486
+ for (const skillName of Object.keys(previousState.skills)) {
1487
+ if (!cloudSkillNames.has(skillName)) {
1488
+ await deps.deleteLocalSkill(skillName, syncPaths);
1489
+ await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
1490
+ deleted += 1;
1491
+ }
1492
+ }
1493
+ const downloaded = await writePulledSkillsToScopedMirror(
1494
+ pullResponse,
1495
+ localSkills,
1496
+ previousState,
1497
+ syncPaths,
1498
+ deps
1499
+ );
1500
+ const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
1501
+ const symlinkResult = await deps.syncSymlinks(
1502
+ buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
1503
+ syncPaths.skillsDir
1504
+ );
1505
+ const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
1506
+ await deps.writeSyncState(
1507
+ buildSyncState(pullResponse, finalLocalSkills, lastSyncedAt),
1508
+ syncPaths
1509
+ );
1510
+ return {
1511
+ syncEnabled: true,
1512
+ pushed: pushCandidates.length,
1513
+ pulled: pullResponse.skills.length,
1514
+ downloaded,
1515
+ deleted,
1516
+ deactivated,
1517
+ linked: symlinkResult.linked,
1518
+ removed: foreignLinksRemoved + symlinkResult.removed,
1519
+ skipped: symlinkResult.skipped,
1520
+ lastSyncedAt,
1521
+ failedPushes
1522
+ };
1523
+ }
1524
+ export {
1525
+ materializeCloudSkillsForLocalShell,
1526
+ runSkillSync
1527
+ };
1528
+ //# sourceMappingURL=index.js.map