@notis_ai/cli 0.2.0-beta.191.1 → 0.2.0-beta.197.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 +485 -86
- package/config/portal-route-roots.json +47 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +32663 -6357
- 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 +3 -3
- 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/handover.js +45 -16
- 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,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import type {
|
|
3
4
|
AgentTargets,
|
|
4
5
|
LocalSkill,
|
|
@@ -6,8 +7,11 @@ import type {
|
|
|
6
7
|
SkillSyncFailure,
|
|
7
8
|
SyncPullResponse,
|
|
8
9
|
SyncSettings,
|
|
10
|
+
SyncedSkill,
|
|
9
11
|
} from "./types";
|
|
10
12
|
import { normalizeAgentTargets } from "./types";
|
|
13
|
+
import { assertNativePull, checkedNativeReference, isNativeSkill, nativeState } from './native-identity';
|
|
14
|
+
import { bindNativeFolders, hasNativeFolderHistory, nativeLinkRows, nativeLocalReady, previousNativeState, retainMissingNativeState } from './native-plan';
|
|
11
15
|
import {
|
|
12
16
|
downloadSkillBundle,
|
|
13
17
|
fetchSyncSettings,
|
|
@@ -59,6 +63,8 @@ export interface RunSkillSyncOptions {
|
|
|
59
63
|
/** Electron's scheduled invocation honors the account preference. A manual
|
|
60
64
|
* CLI sync always runs, even when automatic Desktop refresh is disabled. */
|
|
61
65
|
honorSyncEnabled?: boolean;
|
|
66
|
+
/** All account paths, scanner roots and agent links use this explicit home. */
|
|
67
|
+
syncHome?: string;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
const BASE_SKILL_NAMES = new Set(['notis-apps', 'notis-query', 'notis-cli']);
|
|
@@ -93,6 +99,8 @@ export interface MaterializeCloudSkillsResult {
|
|
|
93
99
|
export interface MaterializeCloudSkillsOptions {
|
|
94
100
|
/** Server-verified Notis identity, when Desktop's auth subject differs. */
|
|
95
101
|
canonicalUserId?: string;
|
|
102
|
+
/** Previously authenticated settings must agree with the pull before I/O. */
|
|
103
|
+
syncSettings?: SyncSettings;
|
|
96
104
|
/** Re-link only these cloud skills without removing or adopting other links. */
|
|
97
105
|
relinkSkillNames?: readonly string[];
|
|
98
106
|
}
|
|
@@ -134,6 +142,22 @@ const DEFAULT_RUN_SKILL_SYNC_DEPS: RunSkillSyncDependencies = {
|
|
|
134
142
|
removeForeignAccountSymlinks,
|
|
135
143
|
updateAgentTargets,
|
|
136
144
|
};
|
|
145
|
+
|
|
146
|
+
export function isolatedLinkOptions(paths: ReturnType<typeof getSkillSyncPathsForUser>) {
|
|
147
|
+
if (!paths.isolatedNamespace && !paths.homeDir) return undefined;
|
|
148
|
+
const root = paths.isolatedNamespace
|
|
149
|
+
? join(paths.syncRoot, 'environments', paths.isolatedNamespace, 'agents') : paths.homeDir!;
|
|
150
|
+
return { agentSkillDirs: { notis: paths.skillsDir, claude_code: join(root, '.claude', 'skills'),
|
|
151
|
+
cursor: join(root, '.cursor', 'skills'), codex: join(root, '.codex', 'skills') },
|
|
152
|
+
legacyGlobalSkillsDir: join(root, '.agents', 'skills') };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function assertSyncEnvironment(pull: SyncPullResponse, settings: SyncSettings): void {
|
|
156
|
+
if ((pull.sync_namespace || null) !== (settings.sync_namespace || null)
|
|
157
|
+
|| (settings.sync_scope !== undefined && pull.sync_scope !== settings.sync_scope)) {
|
|
158
|
+
throw new Error('Skill sync environment changed. Existing mirrors were retained.');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
137
161
|
function toSkillMap(skills: LocalSkill[]): Map<string, LocalSkill> {
|
|
138
162
|
return new Map(skills.map((skill) => [skill.name, skill]));
|
|
139
163
|
}
|
|
@@ -175,6 +199,15 @@ export function shouldWriteCloudSkill(
|
|
|
175
199
|
return true;
|
|
176
200
|
}
|
|
177
201
|
|
|
202
|
+
if (isNativeSkill(cloudSkill)) {
|
|
203
|
+
if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
|
|
204
|
+
if (nativeLocalReady(cloudSkill, localSkill)) return false;
|
|
205
|
+
const nativePrevious = previousNativeState(previousState, cloudSkill);
|
|
206
|
+
return (localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash)
|
|
207
|
+
|| (!localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id
|
|
208
|
+
&& nativePrevious.folderHash === localSkill.folderHash);
|
|
209
|
+
}
|
|
210
|
+
|
|
178
211
|
const previous = previousState.skills[skillName];
|
|
179
212
|
if (previous?.folderHash === localSkill.folderHash
|
|
180
213
|
&& previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
|
|
@@ -182,10 +215,9 @@ export function shouldWriteCloudSkill(
|
|
|
182
215
|
// Without a stored content baseline (skills served through a signed source URL keep
|
|
183
216
|
// none, because that URL changes on every pull) the only other signal is
|
|
184
217
|
// `skill_folder_hash`, which the server does NOT always compute the way the local
|
|
185
|
-
// folder hash is computed.
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// written inside it. Compare cloud against cloud instead: rewrite when the cloud
|
|
218
|
+
// folder hash is computed. A bundle-entry hash can never equal the hash of the
|
|
219
|
+
// extracted directory, and comparing the two replaced the folder on every sync
|
|
220
|
+
// tick, deleting whatever a running skill had written inside it. Compare cloud against cloud instead: rewrite when the cloud
|
|
189
221
|
// revision we last applied has actually moved.
|
|
190
222
|
if (previous
|
|
191
223
|
&& previous.cloudContentHash === undefined
|
|
@@ -257,19 +289,29 @@ function buildSyncState(
|
|
|
257
289
|
failedContentNames: ReadonlySet<string> = new Set(),
|
|
258
290
|
appliedRevisions: AppliedCloudRevisions = {},
|
|
259
291
|
appliedFiles: Record<string, string[] | undefined> = {},
|
|
292
|
+
previousState: NotisSyncState = { version: 1, lastSyncedAt: null, skills: {} },
|
|
260
293
|
): NotisSyncState {
|
|
261
294
|
const localSkillMap = toSkillMap(localSkills);
|
|
262
295
|
// The same row selection the writer used, so the state describes the revision that
|
|
263
296
|
// is actually on disk rather than whichever duplicate row the server listed last.
|
|
264
|
-
const skills = Object.fromEntries(
|
|
265
|
-
selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
|
|
297
|
+
const skills = Object.fromEntries<SyncedSkill>(
|
|
298
|
+
selectCloudSkillsToApply(pullResponse.skills).map((skill): [string, SyncedSkill] => {
|
|
266
299
|
const localSkill = localSkillMap.get(skill.name);
|
|
267
300
|
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
268
301
|
const appliedFileList = appliedFiles[skill.name];
|
|
302
|
+
if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
|
|
303
|
+
const previous = previousNativeState(previousState, skill);
|
|
304
|
+
return [skill.name, { ...(previous || {
|
|
305
|
+
cloudId: skill.id, folderHash: localSkill?.nativeBaseFolderHash || '',
|
|
306
|
+
agentTargets: normalizeAgentTargets(skill.agent_targets), syncedAt: lastSyncedAt || new Date().toISOString(),
|
|
307
|
+
...(localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}),
|
|
308
|
+
}), nativeUnavailable: true, verifiedAgentLinks: {} }];
|
|
309
|
+
}
|
|
269
310
|
return [
|
|
270
311
|
skill.name,
|
|
271
312
|
{
|
|
272
313
|
cloudId: skill.id,
|
|
314
|
+
...nativeState(skill),
|
|
273
315
|
folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
|
|
274
316
|
agentTargets: normalizeAgentTargets(skill.agent_targets),
|
|
275
317
|
verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
|
|
@@ -284,11 +326,18 @@ function buildSyncState(
|
|
|
284
326
|
}),
|
|
285
327
|
);
|
|
286
328
|
|
|
287
|
-
|
|
329
|
+
for (const local of localSkills) {
|
|
330
|
+
if (!local.nativeReference || Object.values(skills).some(row => row.cloudId === local.cloudId)) continue;
|
|
331
|
+
skills[local.name] = { cloudId: local.cloudId!, nativeReference: local.nativeReference,
|
|
332
|
+
nativeScope: local.nativeScope, nativeUnavailable: true, verifiedAgentLinks: {},
|
|
333
|
+
folderHash: local.nativeBaseFolderHash || '', agentTargets: normalizeAgentTargets(),
|
|
334
|
+
syncedAt: lastSyncedAt || new Date().toISOString() };
|
|
335
|
+
}
|
|
336
|
+
return retainMissingNativeState(previousState, {
|
|
288
337
|
version: 1,
|
|
289
338
|
lastSyncedAt,
|
|
290
339
|
skills,
|
|
291
|
-
};
|
|
340
|
+
});
|
|
292
341
|
}
|
|
293
342
|
|
|
294
343
|
function buildLocalSymlinkCandidates(
|
|
@@ -301,6 +350,10 @@ function buildLocalSymlinkCandidates(
|
|
|
301
350
|
);
|
|
302
351
|
const localOnlySkills = localSkills
|
|
303
352
|
.filter((skill) => !cloudSkillNames.has(skill.name))
|
|
353
|
+
.filter(skill => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference
|
|
354
|
+
&& !previousState.skills[skill.name]?.nativeUnavailable
|
|
355
|
+
&& !hasNativeFolderHistory(previousState, skill.name)
|
|
356
|
+
&& !/^native-[a-f0-9-]{36}$/.test(skill.name))
|
|
304
357
|
.map((skill) => {
|
|
305
358
|
const previous = previousState.skills[skill.name];
|
|
306
359
|
return {
|
|
@@ -315,7 +368,7 @@ function buildLocalSymlinkCandidates(
|
|
|
315
368
|
};
|
|
316
369
|
});
|
|
317
370
|
|
|
318
|
-
return [...pullResponse
|
|
371
|
+
return [...nativeLinkRows(pullResponse, localSkills), ...localOnlySkills];
|
|
319
372
|
}
|
|
320
373
|
|
|
321
374
|
function isEmptySyncState(state: NotisSyncState): boolean {
|
|
@@ -364,6 +417,7 @@ async function writePulledSkillsToScopedMirror(
|
|
|
364
417
|
failures: SkillSyncFailure[] = [],
|
|
365
418
|
writtenSkillNames: Set<string> = new Set(),
|
|
366
419
|
appliedFilesByName: Record<string, string[]> = {},
|
|
420
|
+
acknowledgedNative: ReadonlyMap<string, string> = new Map(),
|
|
367
421
|
): Promise<number> {
|
|
368
422
|
const localSkillMap = toSkillMap(localSkills);
|
|
369
423
|
const warnSkillSync = (message: string, error: unknown): void => {
|
|
@@ -372,7 +426,12 @@ async function writePulledSkillsToScopedMirror(
|
|
|
372
426
|
|
|
373
427
|
let downloaded = 0;
|
|
374
428
|
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
375
|
-
|
|
429
|
+
const local = localSkillMap.get(cloudSkill.name);
|
|
430
|
+
const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
|
|
431
|
+
if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
432
|
+
if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
|
|
433
|
+
failures.push({ name: cloudSkill.name, error: 'Local native Skill changes were retained; resolve the version or identity conflict before using them.' });
|
|
434
|
+
}
|
|
376
435
|
continue;
|
|
377
436
|
}
|
|
378
437
|
|
|
@@ -382,7 +441,8 @@ async function writePulledSkillsToScopedMirror(
|
|
|
382
441
|
deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
|
|
383
442
|
// What the last write put there. Everything else in the folder was
|
|
384
443
|
// produced locally and survives this one.
|
|
385
|
-
previouslyApplied: previousState
|
|
444
|
+
previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
|
|
445
|
+
...(isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}),
|
|
386
446
|
}),
|
|
387
447
|
onWarning: warnSkillSync,
|
|
388
448
|
});
|
|
@@ -403,6 +463,8 @@ async function writePulledSkillsToScopedMirror(
|
|
|
403
463
|
function assertSkillsPullAuthorized(
|
|
404
464
|
pullResponse: SyncPullResponse,
|
|
405
465
|
): void {
|
|
466
|
+
if (!Array.isArray(pullResponse.skills)) throw new Error('Skill inventory is unavailable; existing files were retained.');
|
|
467
|
+
assertNativePull(pullResponse);
|
|
406
468
|
if (
|
|
407
469
|
pullResponse.entitlement_access?.code === "entitlement_upgrade_required"
|
|
408
470
|
&& pullResponse.entitlement_access.entitlement === "skills"
|
|
@@ -445,12 +507,14 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
445
507
|
);
|
|
446
508
|
}
|
|
447
509
|
|
|
448
|
-
|
|
449
|
-
const pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
510
|
+
let pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
450
511
|
assertSkillsPullAuthorized(pullResponse);
|
|
512
|
+
if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
|
|
513
|
+
const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, undefined, pullResponse.sync_namespace);
|
|
451
514
|
const previousState = await deps.readSyncState(syncPaths);
|
|
452
515
|
|
|
453
516
|
const localSkills = await deps.scanLocalSkills(syncPaths);
|
|
517
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
454
518
|
const failedDownloads: SkillSyncFailure[] = [];
|
|
455
519
|
const writtenSkillNames = new Set<string>();
|
|
456
520
|
const appliedFilesByName: Record<string, string[]> = {};
|
|
@@ -478,9 +542,9 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
478
542
|
}
|
|
479
543
|
if (relinkSkillNames.size > 0) {
|
|
480
544
|
const relinked = await deps.syncSymlinks(
|
|
481
|
-
pullResponse.
|
|
545
|
+
nativeLinkRows(pullResponse, finalLocalSkills).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
482
546
|
syncPaths.skillsDir,
|
|
483
|
-
{ removeUndesired: false },
|
|
547
|
+
{ ...isolatedLinkOptions(syncPaths), removeUndesired: false },
|
|
484
548
|
);
|
|
485
549
|
failures.push(...(relinked.failures ?? []).filter(
|
|
486
550
|
(failure) => !failedDownloads.some((download) => download.name === failure.name),
|
|
@@ -496,6 +560,7 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
496
560
|
new Set(failedDownloads.map(item => item.name)),
|
|
497
561
|
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
498
562
|
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
563
|
+
previousState,
|
|
499
564
|
);
|
|
500
565
|
// Pull-only refresh is not an upload acknowledgement. Keep content baselines
|
|
501
566
|
// unless we actually wrote cloud content, and retain cloud-missing entries so
|
|
@@ -511,17 +576,27 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
511
576
|
}
|
|
512
577
|
}
|
|
513
578
|
materializedState.skills = { ...previousState.skills, ...materializedState.skills };
|
|
579
|
+
let removed = 0;
|
|
580
|
+
for (const [name, previous] of Object.entries(previousState.skills)) {
|
|
581
|
+
if (!previous.nativeReference && !previous.nativeUnavailable) continue;
|
|
582
|
+
const current = pullResponse.skills.find(skill => skill.id === previous.cloudId);
|
|
583
|
+
if (!current || !nativeLocalReady(current, finalLocalSkills.find(local => local.name === current.name))) {
|
|
584
|
+
removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
514
587
|
await deps.writeSyncState(materializedState, syncPaths);
|
|
515
588
|
|
|
516
589
|
return {
|
|
517
590
|
materializedSkillNames: pullResponse.skills
|
|
518
591
|
.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name)
|
|
592
|
+
&& (!isNativeSkill(skill) || (skill.status === 'active' && skill.agent_targets?.notis !== false
|
|
593
|
+
&& nativeLocalReady(skill, finalLocalSkills.find(local => local.name === skill.name))))
|
|
519
594
|
&& !failedDownloads.some((failure) => failure.name === skill.name))
|
|
520
595
|
.map((skill) => skill.name),
|
|
521
596
|
pulled: pullResponse.skills.length,
|
|
522
597
|
downloaded,
|
|
523
598
|
deleted: 0,
|
|
524
|
-
removed
|
|
599
|
+
removed,
|
|
525
600
|
lastSyncedAt,
|
|
526
601
|
...(failures.length ? { failedLinks: failures } : {}),
|
|
527
602
|
};
|
|
@@ -546,6 +621,7 @@ async function deactivateDeletedAgentSkills(
|
|
|
546
621
|
"detectDeletedAgentSymlinks" | "updateAgentTargets" | "pullSkills"
|
|
547
622
|
>,
|
|
548
623
|
failures: SkillSyncFailure[],
|
|
624
|
+
linkOptions?: ReturnType<typeof isolatedLinkOptions>,
|
|
549
625
|
): Promise<number> {
|
|
550
626
|
// First sync (incl. legacy migration) has no reliable "we created this link" signal, so we
|
|
551
627
|
// cannot tell a user deletion apart from a never-created link — skip detection entirely.
|
|
@@ -554,9 +630,13 @@ async function deactivateDeletedAgentSkills(
|
|
|
554
630
|
}
|
|
555
631
|
|
|
556
632
|
const deletions = await deps.detectDeletedAgentSymlinks(
|
|
557
|
-
pullResponse.skills
|
|
633
|
+
pullResponse.skills.map(skill => {
|
|
634
|
+
const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : undefined;
|
|
635
|
+
return previous ? { ...skill, name: previous[0] } : skill;
|
|
636
|
+
}),
|
|
558
637
|
previousState,
|
|
559
638
|
skillsDir,
|
|
639
|
+
linkOptions,
|
|
560
640
|
);
|
|
561
641
|
if (deletions.length === 0) {
|
|
562
642
|
return 0;
|
|
@@ -574,18 +654,24 @@ async function deactivateDeletedAgentSkills(
|
|
|
574
654
|
|
|
575
655
|
const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
576
656
|
assertSkillsPullAuthorized(fresh);
|
|
657
|
+
assertSyncEnvironment(fresh, pullResponse);
|
|
577
658
|
Object.assign(pullResponse, fresh);
|
|
578
659
|
let needsRefresh = false;
|
|
579
660
|
let deactivated = 0;
|
|
580
661
|
for (const [skillId, { skillName, agents }] of agentsBySkill) {
|
|
581
662
|
const skill = pullResponse.skills.find((item) => item.id === skillId);
|
|
582
663
|
const previous = previousState.skills[skillName];
|
|
583
|
-
if (!skill
|
|
664
|
+
if (!skill) continue;
|
|
665
|
+
const native = isNativeSkill(skill);
|
|
666
|
+
if (native) {
|
|
667
|
+
if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
|
|
668
|
+
} else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
|
|
584
669
|
const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
|
|
585
670
|
try {
|
|
586
|
-
const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch,
|
|
671
|
+
const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch,
|
|
672
|
+
native ? undefined : skill.updated_at, native ? { reference: skill.native_reference!, settingsRevision: skill.native_settings!.revision } : undefined);
|
|
587
673
|
if (saved.success !== true || !saved.updated_at?.trim()
|
|
588
|
-
|| saved.updated_at === skill.updated_at
|
|
674
|
+
|| (!native && saved.updated_at === skill.updated_at)
|
|
589
675
|
|| !['notis', 'claude_code', 'cursor', 'codex'].every((agent) =>
|
|
590
676
|
typeof saved.agent_targets?.[agent as keyof AgentTargets] === 'boolean')
|
|
591
677
|
|| ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
|
|
@@ -593,6 +679,7 @@ async function deactivateDeletedAgentSkills(
|
|
|
593
679
|
}
|
|
594
680
|
skill.agent_targets = saved.agent_targets;
|
|
595
681
|
skill.updated_at = saved.updated_at;
|
|
682
|
+
if (native) needsRefresh = true;
|
|
596
683
|
deactivated += agents.size;
|
|
597
684
|
} catch (error) {
|
|
598
685
|
needsRefresh = true;
|
|
@@ -603,6 +690,7 @@ async function deactivateDeletedAgentSkills(
|
|
|
603
690
|
if (needsRefresh) {
|
|
604
691
|
const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
605
692
|
assertSkillsPullAuthorized(refreshed);
|
|
693
|
+
assertSyncEnvironment(refreshed, pullResponse);
|
|
606
694
|
Object.assign(pullResponse, refreshed);
|
|
607
695
|
}
|
|
608
696
|
return deactivated;
|
|
@@ -633,11 +721,13 @@ export async function runSkillSync(
|
|
|
633
721
|
"Cannot sync skills without a server-verified account identity.",
|
|
634
722
|
);
|
|
635
723
|
}
|
|
636
|
-
const syncPaths = getSkillSyncPathsForUser(syncUserId);
|
|
637
|
-
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
638
|
-
syncPaths.skillsDir,
|
|
639
|
-
);
|
|
724
|
+
const syncPaths = getSkillSyncPathsForUser(syncUserId, undefined, syncSettings.sync_namespace, options.syncHome);
|
|
640
725
|
if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
|
|
726
|
+
// Disabled sync intentionally performs no pull. Only the authenticated
|
|
727
|
+
// settings scope may remove foreign links; account mirrors stay untouched.
|
|
728
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
729
|
+
syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
|
|
730
|
+
);
|
|
641
731
|
return {
|
|
642
732
|
syncEnabled: false,
|
|
643
733
|
pushed: 0,
|
|
@@ -655,6 +745,10 @@ export async function runSkillSync(
|
|
|
655
745
|
|
|
656
746
|
let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
657
747
|
assertSkillsPullAuthorized(pullResponse);
|
|
748
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
749
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
750
|
+
syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
|
|
751
|
+
);
|
|
658
752
|
|
|
659
753
|
const cloudCuratedSkillNames = new Set(
|
|
660
754
|
pullResponse.skills
|
|
@@ -664,50 +758,63 @@ export async function runSkillSync(
|
|
|
664
758
|
const protectedSkillNames = new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
|
|
665
759
|
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
666
760
|
const assignmentFailures: SkillSyncFailure[] = [];
|
|
667
|
-
const deactivated = syncSettings.agent_targets_conditional_updates === true
|
|
761
|
+
const deactivated = !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true
|
|
668
762
|
? await deactivateDeletedAgentSkills(
|
|
669
|
-
serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures,
|
|
763
|
+
serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures, isolatedLinkOptions(syncPaths),
|
|
670
764
|
)
|
|
671
765
|
: 0;
|
|
766
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
672
767
|
const authUserId = decodeJwtSubject(jwt);
|
|
673
768
|
let previousAuthState: NotisSyncState | null = null;
|
|
674
|
-
if (authUserId && authUserId !== syncUserId) {
|
|
675
|
-
const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
|
|
769
|
+
if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
|
|
770
|
+
const previousAuthPaths = getSkillSyncPathsForUser(authUserId, undefined, undefined, options.syncHome);
|
|
676
771
|
previousAuthState = await deps.readSyncState(previousAuthPaths);
|
|
677
772
|
await deps.gatherTopLevelLocalSkills(syncPaths, {
|
|
678
773
|
sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
|
|
679
774
|
protectedSkillNames,
|
|
680
775
|
});
|
|
681
776
|
}
|
|
682
|
-
|
|
683
|
-
protectedSkillNames
|
|
684
|
-
}
|
|
777
|
+
if (!syncPaths.isolatedNamespace) {
|
|
778
|
+
await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
|
|
779
|
+
}
|
|
685
780
|
const localSkills = (await deps.scanLocalSkills(syncPaths))
|
|
686
781
|
.filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
687
782
|
const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
|
|
688
783
|
localSkills,
|
|
689
784
|
scopedState,
|
|
690
|
-
isEmptySyncState(scopedState)
|
|
785
|
+
!syncPaths.isolatedNamespace && isEmptySyncState(scopedState)
|
|
691
786
|
? (!previousAuthState || isEmptySyncState(previousAuthState)
|
|
692
787
|
? await deps.readLegacySyncState(syncPaths)
|
|
693
788
|
: previousAuthState)
|
|
694
789
|
: null,
|
|
695
790
|
));
|
|
791
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
696
792
|
const gatheredSymlinkResult = await deps.syncSymlinks(
|
|
697
793
|
buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
|
|
698
794
|
syncPaths.skillsDir,
|
|
795
|
+
isolatedLinkOptions(syncPaths),
|
|
699
796
|
);
|
|
700
797
|
const pushCandidates = getPushCandidates(
|
|
701
798
|
localSkills,
|
|
702
799
|
previousState,
|
|
703
800
|
cloudCuratedSkillNames,
|
|
704
801
|
new Set(pullResponse.skills.map((skill) => skill.name)),
|
|
705
|
-
|
|
802
|
+
pullResponse.skills,
|
|
706
803
|
);
|
|
707
804
|
|
|
708
805
|
const failedPushes: SkillSyncFailure[] = [];
|
|
806
|
+
const acknowledgedNative = new Map<string, string>();
|
|
807
|
+
let writeLocalSkills = localSkills;
|
|
709
808
|
if (pushCandidates.length > 0) {
|
|
710
809
|
const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
|
|
810
|
+
for (const result of pushResult.skills || []) {
|
|
811
|
+
const saved = result as { id?: string; native_reference?: unknown };
|
|
812
|
+
const candidate = pushCandidates.find(local => local.nativeReference && local.cloudId === saved.id);
|
|
813
|
+
if (candidate && saved.native_reference) {
|
|
814
|
+
checkedNativeReference(saved.native_reference, candidate.cloudId);
|
|
815
|
+
acknowledgedNative.set(candidate.cloudId!, candidate.folderHash);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
711
818
|
if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
|
|
712
819
|
failedPushes.push(...pushResult.failed);
|
|
713
820
|
console.warn(
|
|
@@ -717,6 +824,9 @@ export async function runSkillSync(
|
|
|
717
824
|
}
|
|
718
825
|
pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
719
826
|
assertSkillsPullAuthorized(pullResponse);
|
|
827
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
828
|
+
writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter(skill => !BASE_SKILL_NAMES.has(skill.name));
|
|
829
|
+
pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
|
|
720
830
|
}
|
|
721
831
|
|
|
722
832
|
// Delete phase: remove skills that were previously synced but are no longer in the cloud
|
|
@@ -724,8 +834,14 @@ export async function runSkillSync(
|
|
|
724
834
|
let deleted = 0;
|
|
725
835
|
for (const skillName of Object.keys(previousState.skills)) {
|
|
726
836
|
if (!cloudSkillNames.has(skillName)) {
|
|
837
|
+
const previous = previousState.skills[skillName];
|
|
838
|
+
if (previous.nativeReference || previous.nativeUnavailable
|
|
839
|
+
|| pullResponse.skills.some(skill => isNativeSkill(skill) && skill.id === previous.cloudId)) {
|
|
840
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
727
843
|
await deps.deleteLocalSkill(skillName, syncPaths);
|
|
728
|
-
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
|
|
844
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
729
845
|
deleted += 1;
|
|
730
846
|
}
|
|
731
847
|
}
|
|
@@ -735,13 +851,14 @@ export async function runSkillSync(
|
|
|
735
851
|
const appliedFilesByName: Record<string, string[]> = {};
|
|
736
852
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
737
853
|
pullResponse,
|
|
738
|
-
|
|
854
|
+
writeLocalSkills,
|
|
739
855
|
previousState,
|
|
740
856
|
syncPaths,
|
|
741
857
|
deps,
|
|
742
858
|
failedDownloads,
|
|
743
859
|
writtenSkillNames,
|
|
744
860
|
appliedFilesByName,
|
|
861
|
+
acknowledgedNative,
|
|
745
862
|
);
|
|
746
863
|
|
|
747
864
|
const finalLocalSkills = (await deps.scanLocalSkills(syncPaths))
|
|
@@ -749,6 +866,7 @@ export async function runSkillSync(
|
|
|
749
866
|
const symlinkResult = await deps.syncSymlinks(
|
|
750
867
|
buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
|
|
751
868
|
syncPaths.skillsDir,
|
|
869
|
+
isolatedLinkOptions(syncPaths),
|
|
752
870
|
);
|
|
753
871
|
const verifiedLinks = { ...(symlinkResult.verifiedAgentLinks ?? {}) };
|
|
754
872
|
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
@@ -763,6 +881,7 @@ export async function runSkillSync(
|
|
|
763
881
|
new Set(failedDownloads.map(item => item.name)),
|
|
764
882
|
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
765
883
|
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
884
|
+
previousState,
|
|
766
885
|
),
|
|
767
886
|
syncPaths,
|
|
768
887
|
);
|