@notis_ai/cli 0.2.0-beta.178.1 → 0.2.0-beta.1792022492092.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +548 -98
- package/config/portal-route-roots.json +47 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +32901 -5934
- package/dist/base-skills/notis-apps/SKILL.md +76 -51
- package/dist/base-skills/notis-apps/references/architecture.md +116 -164
- package/dist/base-skills/notis-apps/references/design.md +26 -107
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +52 -74
- package/dist/base-skills/notis-apps/references/release.md +105 -96
- package/dist/base-skills/notis-apps/references/sdk.md +115 -60
- package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
- package/dist/base-skills/notis-apps/references/views.md +145 -0
- package/dist/base-skills/notis-cli/SKILL.md +29 -12
- package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +6 -5
- package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
- package/dist/base-skills/notis-query/SKILL.md +34 -60
- package/dist/base-skills/notis-query/references/database-discovery.md +60 -59
- package/dist/base-skills/notis-query/references/documents.md +53 -39
- package/dist/base-skills/notis-query/references/query.md +57 -524
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
- package/dist/sdk/src/components/DocumentPage.tsx +144 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/config.ts +4 -26
- package/dist/sdk/src/documents.ts +2 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useBackend.ts +3 -2
- package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +4 -2
- package/dist/sdk/src/hooks/useHandover.ts +7 -7
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +3 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
- package/dist/sdk/src/hooks/useQuery.ts +7 -4
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +36 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +42 -5
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +5 -3
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +163 -13
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +96 -72
- package/dist/sdk/src/vite.ts +11 -1
- package/dist/skill-sync/index.js +571 -80
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +858 -233
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1028 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +175 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +16 -5
- package/skills/notis-apps/cli.md +323 -163
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +3 -3
- package/skills/notis-onboarding/BRIEF.md +7 -3
- package/src/command-specs/apps.js +2 -1
- package/src/command-specs/auth.js +2 -2
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +331 -79
- package/src/command-specs/helpers.js +2 -0
- package/src/command-specs/index.js +11 -5
- package/src/command-specs/profile.js +42 -1
- package/src/command-specs/skills.js +242 -1
- package/src/command-specs/spaces.js +645 -0
- package/src/command-specs/tools.js +0 -6
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +92 -29
- package/src/runtime/app-boundary-validator.js +7 -4
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +321 -253
- package/src/runtime/computer-identity.d.ts +30 -0
- package/src/runtime/computer-identity.js +151 -0
- package/src/runtime/config-lock.js +135 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +5 -21
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +18 -1
- package/src/runtime/profiles.js +5 -107
- package/src/runtime/retired-sdk-files.js +20 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/skill-sync/cloud-client.ts +26 -4
- package/src/runtime/skill-sync/index.ts +230 -35
- package/src/runtime/skill-sync/local-scanner.ts +188 -29
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +70 -0
- package/src/runtime/skill-sync/sync-plan.ts +16 -9
- package/src/runtime/skill-sync/types.ts +32 -6
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/store-screenshot.js +10 -7
- package/src/runtime/sync-skills.d.ts +3 -1
- package/src/runtime/sync-skills.js +10 -2
- package/src/runtime/transport.js +8 -6
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker.js +14 -2
- package/src/space-harness-host.ts +103 -0
- package/template/.harness/index.html.tmpl +7 -10
- package/template/app/globals.css +1 -0
- package/src/command-specs/reports.js +0 -97
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
2
4
|
import type {
|
|
3
5
|
AgentTargets,
|
|
4
6
|
LocalSkill,
|
|
@@ -6,8 +8,11 @@ import type {
|
|
|
6
8
|
SkillSyncFailure,
|
|
7
9
|
SyncPullResponse,
|
|
8
10
|
SyncSettings,
|
|
11
|
+
SyncedSkill,
|
|
9
12
|
} from "./types";
|
|
10
13
|
import { normalizeAgentTargets } from "./types";
|
|
14
|
+
import { assertNativePull, checkedNativeReference, isNativeSkill, nativeState } from './native-identity';
|
|
15
|
+
import { bindNativeFolders, hasNativeFolderHistory, nativeLinkRows, nativeLocalReady, pendingLegacyUpgrade, previousNativeState, retainMissingNativeState } from './native-plan';
|
|
11
16
|
import {
|
|
12
17
|
downloadSkillBundle,
|
|
13
18
|
fetchSyncSettings,
|
|
@@ -59,6 +64,12 @@ export interface RunSkillSyncOptions {
|
|
|
59
64
|
/** Electron's scheduled invocation honors the account preference. A manual
|
|
60
65
|
* CLI sync always runs, even when automatic Desktop refresh is disabled. */
|
|
61
66
|
honorSyncEnabled?: boolean;
|
|
67
|
+
/** All account paths, scanner roots and agent links use this explicit home. */
|
|
68
|
+
syncHome?: string;
|
|
69
|
+
/** False when another account is the CLI default: sync this account's own
|
|
70
|
+
* folder only and leave the agent folders (Claude Code, Codex, Cursor) to the
|
|
71
|
+
* default's skills. Defaults to true. */
|
|
72
|
+
linkAgentFolders?: boolean;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
const BASE_SKILL_NAMES = new Set(['notis-apps', 'notis-query', 'notis-cli']);
|
|
@@ -93,6 +104,8 @@ export interface MaterializeCloudSkillsResult {
|
|
|
93
104
|
export interface MaterializeCloudSkillsOptions {
|
|
94
105
|
/** Server-verified Notis identity, when Desktop's auth subject differs. */
|
|
95
106
|
canonicalUserId?: string;
|
|
107
|
+
/** Previously authenticated settings must agree with the pull before I/O. */
|
|
108
|
+
syncSettings?: SyncSettings;
|
|
96
109
|
/** Re-link only these cloud skills without removing or adopting other links. */
|
|
97
110
|
relinkSkillNames?: readonly string[];
|
|
98
111
|
}
|
|
@@ -134,6 +147,74 @@ const DEFAULT_RUN_SKILL_SYNC_DEPS: RunSkillSyncDependencies = {
|
|
|
134
147
|
removeForeignAccountSymlinks,
|
|
135
148
|
updateAgentTargets,
|
|
136
149
|
};
|
|
150
|
+
|
|
151
|
+
// Another account is the CLI default and owns the agent folders: this account
|
|
152
|
+
// syncs its own folder without linking, unlinking or adopting anything there.
|
|
153
|
+
const MIRROR_ONLY_DEPS: Pick<
|
|
154
|
+
RunSkillSyncDependencies,
|
|
155
|
+
'removeForeignAccountSymlinks' | 'removeAllSymlinksForSkill' | 'syncSymlinks'
|
|
156
|
+
> = {
|
|
157
|
+
removeForeignAccountSymlinks: async () => 0,
|
|
158
|
+
removeAllSymlinksForSkill: async () => 0,
|
|
159
|
+
syncSymlinks: async () => ({ linked: 0, removed: 0, skipped: 0, verifiedAgentLinks: {} }),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The account linking now owns the agent folders, so the links the other
|
|
164
|
+
* accounts on this computer last verified are gone. Forget them: otherwise that
|
|
165
|
+
* account's next linking sync would read each missing link as the user removing
|
|
166
|
+
* the skill and deactivate it.
|
|
167
|
+
*/
|
|
168
|
+
async function forgetOtherAccountsAgentLinks(
|
|
169
|
+
paths: ReturnType<typeof getSkillSyncPathsForUser>,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const usersDir = dirname(dirname(paths.syncStatePath));
|
|
172
|
+
let entries: import('node:fs').Dirent[];
|
|
173
|
+
try {
|
|
174
|
+
entries = await fs.readdir(usersDir, { withFileTypes: true });
|
|
175
|
+
} catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (!entry.isDirectory()) continue;
|
|
180
|
+
const statePath = join(usersDir, entry.name, '.notis-sync.json');
|
|
181
|
+
if (statePath === paths.syncStatePath) continue;
|
|
182
|
+
let state: { skills?: Record<string, { verifiedAgentLinks?: unknown }> } | null = null;
|
|
183
|
+
try {
|
|
184
|
+
state = JSON.parse(await fs.readFile(statePath, 'utf8'));
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!state || typeof state !== 'object' || !state.skills || typeof state.skills !== 'object') continue;
|
|
189
|
+
let changed = false;
|
|
190
|
+
for (const skill of Object.values(state.skills)) {
|
|
191
|
+
if (skill && typeof skill === 'object' && 'verifiedAgentLinks' in skill) {
|
|
192
|
+
delete skill.verifiedAgentLinks;
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!changed) continue;
|
|
197
|
+
const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
198
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
199
|
+
await fs.rename(temporaryPath, statePath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function isolatedLinkOptions(paths: ReturnType<typeof getSkillSyncPathsForUser>) {
|
|
204
|
+
if (!paths.isolatedNamespace && !paths.homeDir) return undefined;
|
|
205
|
+
const root = paths.isolatedNamespace
|
|
206
|
+
? join(paths.syncRoot, 'environments', paths.isolatedNamespace, 'agents') : paths.homeDir!;
|
|
207
|
+
return { agentSkillDirs: { notis: paths.skillsDir, claude_code: join(root, '.claude', 'skills'),
|
|
208
|
+
cursor: join(root, '.cursor', 'skills'), codex: join(root, '.codex', 'skills') },
|
|
209
|
+
legacyGlobalSkillsDir: join(root, '.agents', 'skills') };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function assertSyncEnvironment(pull: SyncPullResponse, settings: SyncSettings): void {
|
|
213
|
+
if ((pull.sync_namespace || null) !== (settings.sync_namespace || null)
|
|
214
|
+
|| (settings.sync_scope !== undefined && pull.sync_scope !== settings.sync_scope)) {
|
|
215
|
+
throw new Error('Skill sync environment changed. Existing mirrors were retained.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
137
218
|
function toSkillMap(skills: LocalSkill[]): Map<string, LocalSkill> {
|
|
138
219
|
return new Map(skills.map((skill) => [skill.name, skill]));
|
|
139
220
|
}
|
|
@@ -175,6 +256,15 @@ export function shouldWriteCloudSkill(
|
|
|
175
256
|
return true;
|
|
176
257
|
}
|
|
177
258
|
|
|
259
|
+
if (isNativeSkill(cloudSkill)) {
|
|
260
|
+
if (localSkill.nativeScope && localSkill.nativeScope !== cloudSkill.native_scope) return false;
|
|
261
|
+
if (nativeLocalReady(cloudSkill, localSkill)) return false;
|
|
262
|
+
const nativePrevious = previousNativeState(previousState, cloudSkill);
|
|
263
|
+
return (localSkill.nativeFilesCurrent === true && localSkill.folderHash === localSkill.nativeBaseFolderHash)
|
|
264
|
+
|| (!localSkill.nativeReference && nativePrevious?.cloudId === cloudSkill.id
|
|
265
|
+
&& nativePrevious.folderHash === localSkill.folderHash);
|
|
266
|
+
}
|
|
267
|
+
|
|
178
268
|
const previous = previousState.skills[skillName];
|
|
179
269
|
if (previous?.folderHash === localSkill.folderHash
|
|
180
270
|
&& previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
|
|
@@ -182,10 +272,9 @@ export function shouldWriteCloudSkill(
|
|
|
182
272
|
// Without a stored content baseline (skills served through a signed source URL keep
|
|
183
273
|
// none, because that URL changes on every pull) the only other signal is
|
|
184
274
|
// `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
|
|
275
|
+
// folder hash is computed. A bundle-entry hash can never equal the hash of the
|
|
276
|
+
// extracted directory, and comparing the two replaced the folder on every sync
|
|
277
|
+
// tick, deleting whatever a running skill had written inside it. Compare cloud against cloud instead: rewrite when the cloud
|
|
189
278
|
// revision we last applied has actually moved.
|
|
190
279
|
if (previous
|
|
191
280
|
&& previous.cloudContentHash === undefined
|
|
@@ -257,19 +346,34 @@ function buildSyncState(
|
|
|
257
346
|
failedContentNames: ReadonlySet<string> = new Set(),
|
|
258
347
|
appliedRevisions: AppliedCloudRevisions = {},
|
|
259
348
|
appliedFiles: Record<string, string[] | undefined> = {},
|
|
349
|
+
previousState: NotisSyncState = { version: 1, lastSyncedAt: null, skills: {} },
|
|
260
350
|
): NotisSyncState {
|
|
261
351
|
const localSkillMap = toSkillMap(localSkills);
|
|
262
352
|
// The same row selection the writer used, so the state describes the revision that
|
|
263
353
|
// is actually on disk rather than whichever duplicate row the server listed last.
|
|
264
|
-
const skills = Object.fromEntries(
|
|
265
|
-
selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
|
|
354
|
+
const skills = Object.fromEntries<SyncedSkill>(
|
|
355
|
+
selectCloudSkillsToApply(pullResponse.skills).map((skill): [string, SyncedSkill] => {
|
|
266
356
|
const localSkill = localSkillMap.get(skill.name);
|
|
267
357
|
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
268
358
|
const appliedFileList = appliedFiles[skill.name];
|
|
359
|
+
if (pendingLegacyUpgrade(skill, localSkill, previousState)) {
|
|
360
|
+
// Keep the legacy baseline so the upgrade retries; never mark a usable folder unavailable.
|
|
361
|
+
const { nativeUnavailable: _unavailable, ...previous } = previousNativeState(previousState, skill)!;
|
|
362
|
+
return [skill.name, { ...previous, verifiedAgentLinks: verifiedAgentLinks[skill.name] ?? {} }];
|
|
363
|
+
}
|
|
364
|
+
if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
|
|
365
|
+
const previous = previousNativeState(previousState, skill);
|
|
366
|
+
return [skill.name, { ...(previous || {
|
|
367
|
+
cloudId: skill.id, folderHash: localSkill?.nativeBaseFolderHash || '',
|
|
368
|
+
agentTargets: normalizeAgentTargets(skill.agent_targets), syncedAt: lastSyncedAt || new Date().toISOString(),
|
|
369
|
+
...(localSkill?.nativeReference ? { nativeReference: localSkill.nativeReference, nativeScope: localSkill.nativeScope } : {}),
|
|
370
|
+
}), nativeUnavailable: true, verifiedAgentLinks: {} }];
|
|
371
|
+
}
|
|
269
372
|
return [
|
|
270
373
|
skill.name,
|
|
271
374
|
{
|
|
272
375
|
cloudId: skill.id,
|
|
376
|
+
...nativeState(skill),
|
|
273
377
|
folderHash: localSkill?.folderHash || skill.skill_folder_hash || "",
|
|
274
378
|
agentTargets: normalizeAgentTargets(skill.agent_targets),
|
|
275
379
|
verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
|
|
@@ -284,11 +388,18 @@ function buildSyncState(
|
|
|
284
388
|
}),
|
|
285
389
|
);
|
|
286
390
|
|
|
287
|
-
|
|
391
|
+
for (const local of localSkills) {
|
|
392
|
+
if (!local.nativeReference || Object.values(skills).some(row => row.cloudId === local.cloudId)) continue;
|
|
393
|
+
skills[local.name] = { cloudId: local.cloudId!, nativeReference: local.nativeReference,
|
|
394
|
+
nativeScope: local.nativeScope, nativeUnavailable: true, verifiedAgentLinks: {},
|
|
395
|
+
folderHash: local.nativeBaseFolderHash || '', agentTargets: normalizeAgentTargets(),
|
|
396
|
+
syncedAt: lastSyncedAt || new Date().toISOString() };
|
|
397
|
+
}
|
|
398
|
+
return retainMissingNativeState(previousState, {
|
|
288
399
|
version: 1,
|
|
289
400
|
lastSyncedAt,
|
|
290
401
|
skills,
|
|
291
|
-
};
|
|
402
|
+
});
|
|
292
403
|
}
|
|
293
404
|
|
|
294
405
|
function buildLocalSymlinkCandidates(
|
|
@@ -301,6 +412,10 @@ function buildLocalSymlinkCandidates(
|
|
|
301
412
|
);
|
|
302
413
|
const localOnlySkills = localSkills
|
|
303
414
|
.filter((skill) => !cloudSkillNames.has(skill.name))
|
|
415
|
+
.filter(skill => !skill.nativeReference && !previousState.skills[skill.name]?.nativeReference
|
|
416
|
+
&& !previousState.skills[skill.name]?.nativeUnavailable
|
|
417
|
+
&& !hasNativeFolderHistory(previousState, skill.name)
|
|
418
|
+
&& !/^native-[a-f0-9-]{36}$/.test(skill.name))
|
|
304
419
|
.map((skill) => {
|
|
305
420
|
const previous = previousState.skills[skill.name];
|
|
306
421
|
return {
|
|
@@ -315,7 +430,7 @@ function buildLocalSymlinkCandidates(
|
|
|
315
430
|
};
|
|
316
431
|
});
|
|
317
432
|
|
|
318
|
-
return [...pullResponse
|
|
433
|
+
return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
|
|
319
434
|
}
|
|
320
435
|
|
|
321
436
|
function isEmptySyncState(state: NotisSyncState): boolean {
|
|
@@ -364,6 +479,7 @@ async function writePulledSkillsToScopedMirror(
|
|
|
364
479
|
failures: SkillSyncFailure[] = [],
|
|
365
480
|
writtenSkillNames: Set<string> = new Set(),
|
|
366
481
|
appliedFilesByName: Record<string, string[]> = {},
|
|
482
|
+
acknowledgedNative: ReadonlyMap<string, string> = new Map(),
|
|
367
483
|
): Promise<number> {
|
|
368
484
|
const localSkillMap = toSkillMap(localSkills);
|
|
369
485
|
const warnSkillSync = (message: string, error: unknown): void => {
|
|
@@ -372,7 +488,16 @@ async function writePulledSkillsToScopedMirror(
|
|
|
372
488
|
|
|
373
489
|
let downloaded = 0;
|
|
374
490
|
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
375
|
-
|
|
491
|
+
const local = localSkillMap.get(cloudSkill.name);
|
|
492
|
+
const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
|
|
493
|
+
// An unchanged pre-native folder has no applied-file record, so the files it shares
|
|
494
|
+
// with the incoming native Skill are the previous sync's output, not local edits.
|
|
495
|
+
const legacyBaseline = Boolean(local && pendingLegacyUpgrade(cloudSkill, local, previousState)
|
|
496
|
+
&& previousNativeState(previousState, cloudSkill)?.folderHash === local.folderHash);
|
|
497
|
+
if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
498
|
+
if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
|
|
499
|
+
failures.push({ name: cloudSkill.name, error: 'Local native Skill changes were retained; resolve the version or identity conflict before using them.' });
|
|
500
|
+
}
|
|
376
501
|
continue;
|
|
377
502
|
}
|
|
378
503
|
|
|
@@ -382,7 +507,9 @@ async function writePulledSkillsToScopedMirror(
|
|
|
382
507
|
deps.writeCloudSkillToDisk(skill, bundleBytes, syncPaths, {
|
|
383
508
|
// What the last write put there. Everything else in the folder was
|
|
384
509
|
// produced locally and survives this one.
|
|
385
|
-
previouslyApplied: previousState
|
|
510
|
+
previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
|
|
511
|
+
...(isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}),
|
|
512
|
+
...(legacyBaseline ? { legacyBaseline } : {}),
|
|
386
513
|
}),
|
|
387
514
|
onWarning: warnSkillSync,
|
|
388
515
|
});
|
|
@@ -403,6 +530,8 @@ async function writePulledSkillsToScopedMirror(
|
|
|
403
530
|
function assertSkillsPullAuthorized(
|
|
404
531
|
pullResponse: SyncPullResponse,
|
|
405
532
|
): void {
|
|
533
|
+
if (!Array.isArray(pullResponse.skills)) throw new Error('Skill inventory is unavailable; existing files were retained.');
|
|
534
|
+
assertNativePull(pullResponse);
|
|
406
535
|
if (
|
|
407
536
|
pullResponse.entitlement_access?.code === "entitlement_upgrade_required"
|
|
408
537
|
&& pullResponse.entitlement_access.entitlement === "skills"
|
|
@@ -445,12 +574,14 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
445
574
|
);
|
|
446
575
|
}
|
|
447
576
|
|
|
448
|
-
|
|
449
|
-
const pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
577
|
+
let pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
450
578
|
assertSkillsPullAuthorized(pullResponse);
|
|
579
|
+
if (options.syncSettings) assertSyncEnvironment(pullResponse, options.syncSettings);
|
|
580
|
+
const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId, undefined, pullResponse.sync_namespace);
|
|
451
581
|
const previousState = await deps.readSyncState(syncPaths);
|
|
452
582
|
|
|
453
583
|
const localSkills = await deps.scanLocalSkills(syncPaths);
|
|
584
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
454
585
|
const failedDownloads: SkillSyncFailure[] = [];
|
|
455
586
|
const writtenSkillNames = new Set<string>();
|
|
456
587
|
const appliedFilesByName: Record<string, string[]> = {};
|
|
@@ -478,9 +609,9 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
478
609
|
}
|
|
479
610
|
if (relinkSkillNames.size > 0) {
|
|
480
611
|
const relinked = await deps.syncSymlinks(
|
|
481
|
-
pullResponse.
|
|
612
|
+
nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
482
613
|
syncPaths.skillsDir,
|
|
483
|
-
{ removeUndesired: false },
|
|
614
|
+
{ ...isolatedLinkOptions(syncPaths), removeUndesired: false },
|
|
484
615
|
);
|
|
485
616
|
failures.push(...(relinked.failures ?? []).filter(
|
|
486
617
|
(failure) => !failedDownloads.some((download) => download.name === failure.name),
|
|
@@ -496,6 +627,7 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
496
627
|
new Set(failedDownloads.map(item => item.name)),
|
|
497
628
|
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
498
629
|
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
630
|
+
previousState,
|
|
499
631
|
);
|
|
500
632
|
// Pull-only refresh is not an upload acknowledgement. Keep content baselines
|
|
501
633
|
// unless we actually wrote cloud content, and retain cloud-missing entries so
|
|
@@ -511,17 +643,28 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
511
643
|
}
|
|
512
644
|
}
|
|
513
645
|
materializedState.skills = { ...previousState.skills, ...materializedState.skills };
|
|
646
|
+
let removed = 0;
|
|
647
|
+
for (const [name, previous] of Object.entries(previousState.skills)) {
|
|
648
|
+
if (!previous.nativeReference && !previous.nativeUnavailable) continue;
|
|
649
|
+
const current = pullResponse.skills.find(skill => skill.id === previous.cloudId);
|
|
650
|
+
const local = current && finalLocalSkills.find(row => row.name === current.name);
|
|
651
|
+
if (!current || (!nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState))) {
|
|
652
|
+
removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
514
655
|
await deps.writeSyncState(materializedState, syncPaths);
|
|
515
656
|
|
|
516
657
|
return {
|
|
517
658
|
materializedSkillNames: pullResponse.skills
|
|
518
659
|
.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name)
|
|
660
|
+
&& (!isNativeSkill(skill) || (skill.status === 'active' && skill.agent_targets?.notis !== false
|
|
661
|
+
&& nativeLocalReady(skill, finalLocalSkills.find(local => local.name === skill.name))))
|
|
519
662
|
&& !failedDownloads.some((failure) => failure.name === skill.name))
|
|
520
663
|
.map((skill) => skill.name),
|
|
521
664
|
pulled: pullResponse.skills.length,
|
|
522
665
|
downloaded,
|
|
523
666
|
deleted: 0,
|
|
524
|
-
removed
|
|
667
|
+
removed,
|
|
525
668
|
lastSyncedAt,
|
|
526
669
|
...(failures.length ? { failedLinks: failures } : {}),
|
|
527
670
|
};
|
|
@@ -546,6 +689,7 @@ async function deactivateDeletedAgentSkills(
|
|
|
546
689
|
"detectDeletedAgentSymlinks" | "updateAgentTargets" | "pullSkills"
|
|
547
690
|
>,
|
|
548
691
|
failures: SkillSyncFailure[],
|
|
692
|
+
linkOptions?: ReturnType<typeof isolatedLinkOptions>,
|
|
549
693
|
): Promise<number> {
|
|
550
694
|
// First sync (incl. legacy migration) has no reliable "we created this link" signal, so we
|
|
551
695
|
// cannot tell a user deletion apart from a never-created link — skip detection entirely.
|
|
@@ -554,9 +698,13 @@ async function deactivateDeletedAgentSkills(
|
|
|
554
698
|
}
|
|
555
699
|
|
|
556
700
|
const deletions = await deps.detectDeletedAgentSymlinks(
|
|
557
|
-
pullResponse.skills
|
|
701
|
+
pullResponse.skills.map(skill => {
|
|
702
|
+
const previous = isNativeSkill(skill) ? Object.entries(previousState.skills).find(([, row]) => row.cloudId === skill.id) : undefined;
|
|
703
|
+
return previous ? { ...skill, name: previous[0] } : skill;
|
|
704
|
+
}),
|
|
558
705
|
previousState,
|
|
559
706
|
skillsDir,
|
|
707
|
+
linkOptions,
|
|
560
708
|
);
|
|
561
709
|
if (deletions.length === 0) {
|
|
562
710
|
return 0;
|
|
@@ -574,18 +722,24 @@ async function deactivateDeletedAgentSkills(
|
|
|
574
722
|
|
|
575
723
|
const fresh = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
576
724
|
assertSkillsPullAuthorized(fresh);
|
|
725
|
+
assertSyncEnvironment(fresh, pullResponse);
|
|
577
726
|
Object.assign(pullResponse, fresh);
|
|
578
727
|
let needsRefresh = false;
|
|
579
728
|
let deactivated = 0;
|
|
580
729
|
for (const [skillId, { skillName, agents }] of agentsBySkill) {
|
|
581
730
|
const skill = pullResponse.skills.find((item) => item.id === skillId);
|
|
582
731
|
const previous = previousState.skills[skillName];
|
|
583
|
-
if (!skill
|
|
732
|
+
if (!skill) continue;
|
|
733
|
+
const native = isNativeSkill(skill);
|
|
734
|
+
if (native) {
|
|
735
|
+
if (!skill.native_settings || previous?.nativeSettingsRevision !== skill.native_settings.revision) continue;
|
|
736
|
+
} else if (!skill.updated_at || previous?.cloudUpdatedAt !== skill.updated_at) continue;
|
|
584
737
|
const patch = Object.fromEntries([...agents].map((agent) => [agent, false]));
|
|
585
738
|
try {
|
|
586
|
-
const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch,
|
|
739
|
+
const saved = await deps.updateAgentTargets(serverUrl, jwt, skillId, patch,
|
|
740
|
+
native ? undefined : skill.updated_at, native ? { reference: skill.native_reference!, settingsRevision: skill.native_settings!.revision } : undefined);
|
|
587
741
|
if (saved.success !== true || !saved.updated_at?.trim()
|
|
588
|
-
|| saved.updated_at === skill.updated_at
|
|
742
|
+
|| (!native && saved.updated_at === skill.updated_at)
|
|
589
743
|
|| !['notis', 'claude_code', 'cursor', 'codex'].every((agent) =>
|
|
590
744
|
typeof saved.agent_targets?.[agent as keyof AgentTargets] === 'boolean')
|
|
591
745
|
|| ![...agents].every((agent) => saved.agent_targets[agent] === false)) {
|
|
@@ -593,6 +747,7 @@ async function deactivateDeletedAgentSkills(
|
|
|
593
747
|
}
|
|
594
748
|
skill.agent_targets = saved.agent_targets;
|
|
595
749
|
skill.updated_at = saved.updated_at;
|
|
750
|
+
if (native) needsRefresh = true;
|
|
596
751
|
deactivated += agents.size;
|
|
597
752
|
} catch (error) {
|
|
598
753
|
needsRefresh = true;
|
|
@@ -603,6 +758,7 @@ async function deactivateDeletedAgentSkills(
|
|
|
603
758
|
if (needsRefresh) {
|
|
604
759
|
const refreshed = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
605
760
|
assertSkillsPullAuthorized(refreshed);
|
|
761
|
+
assertSyncEnvironment(refreshed, pullResponse);
|
|
606
762
|
Object.assign(pullResponse, refreshed);
|
|
607
763
|
}
|
|
608
764
|
return deactivated;
|
|
@@ -614,9 +770,11 @@ export async function runSkillSync(
|
|
|
614
770
|
dependencies: Partial<RunSkillSyncDependencies> = {},
|
|
615
771
|
options: RunSkillSyncOptions = {},
|
|
616
772
|
): Promise<RunSkillSyncResult> {
|
|
773
|
+
const linkAgentFolders = options.linkAgentFolders !== false;
|
|
617
774
|
const deps = {
|
|
618
775
|
...DEFAULT_RUN_SKILL_SYNC_DEPS,
|
|
619
776
|
...dependencies,
|
|
777
|
+
...(linkAgentFolders ? {} : MIRROR_ONLY_DEPS),
|
|
620
778
|
};
|
|
621
779
|
const syncSettings: SyncSettings = await deps.fetchSyncSettings(
|
|
622
780
|
serverUrl,
|
|
@@ -633,11 +791,13 @@ export async function runSkillSync(
|
|
|
633
791
|
"Cannot sync skills without a server-verified account identity.",
|
|
634
792
|
);
|
|
635
793
|
}
|
|
636
|
-
const syncPaths = getSkillSyncPathsForUser(syncUserId);
|
|
637
|
-
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
638
|
-
syncPaths.skillsDir,
|
|
639
|
-
);
|
|
794
|
+
const syncPaths = getSkillSyncPathsForUser(syncUserId, undefined, syncSettings.sync_namespace, options.syncHome);
|
|
640
795
|
if (options.honorSyncEnabled !== false && !syncSettings.sync_enabled) {
|
|
796
|
+
// Disabled sync intentionally performs no pull. Only the authenticated
|
|
797
|
+
// settings scope may remove foreign links; account mirrors stay untouched.
|
|
798
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
799
|
+
syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
|
|
800
|
+
);
|
|
641
801
|
return {
|
|
642
802
|
syncEnabled: false,
|
|
643
803
|
pushed: 0,
|
|
@@ -655,6 +815,13 @@ export async function runSkillSync(
|
|
|
655
815
|
|
|
656
816
|
let pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
657
817
|
assertSkillsPullAuthorized(pullResponse);
|
|
818
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
819
|
+
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
820
|
+
syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
|
|
821
|
+
);
|
|
822
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
823
|
+
await forgetOtherAccountsAgentLinks(syncPaths);
|
|
824
|
+
}
|
|
658
825
|
|
|
659
826
|
const cloudCuratedSkillNames = new Set(
|
|
660
827
|
pullResponse.skills
|
|
@@ -664,50 +831,66 @@ export async function runSkillSync(
|
|
|
664
831
|
const protectedSkillNames = new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
|
|
665
832
|
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
666
833
|
const assignmentFailures: SkillSyncFailure[] = [];
|
|
667
|
-
|
|
834
|
+
// A mirror-only sync has no links by design, so none of them is missing.
|
|
835
|
+
const deactivated = linkAgentFolders && !syncPaths.isolatedNamespace
|
|
836
|
+
&& syncSettings.agent_targets_conditional_updates === true
|
|
668
837
|
? await deactivateDeletedAgentSkills(
|
|
669
|
-
serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures,
|
|
838
|
+
serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures, isolatedLinkOptions(syncPaths),
|
|
670
839
|
)
|
|
671
840
|
: 0;
|
|
841
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
672
842
|
const authUserId = decodeJwtSubject(jwt);
|
|
673
843
|
let previousAuthState: NotisSyncState | null = null;
|
|
674
|
-
if (authUserId && authUserId !== syncUserId) {
|
|
675
|
-
const previousAuthPaths = getSkillSyncPathsForUser(authUserId);
|
|
844
|
+
if (!syncPaths.isolatedNamespace && authUserId && authUserId !== syncUserId) {
|
|
845
|
+
const previousAuthPaths = getSkillSyncPathsForUser(authUserId, undefined, undefined, options.syncHome);
|
|
676
846
|
previousAuthState = await deps.readSyncState(previousAuthPaths);
|
|
677
847
|
await deps.gatherTopLevelLocalSkills(syncPaths, {
|
|
678
848
|
sourceRoots: [{ label: "previous-auth-scope", root: previousAuthPaths.skillsDir }],
|
|
679
849
|
protectedSkillNames,
|
|
680
850
|
});
|
|
681
851
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
852
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
853
|
+
// Skills found loose in the agent folders belong to the account linking them.
|
|
854
|
+
await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
|
|
855
|
+
}
|
|
685
856
|
const localSkills = (await deps.scanLocalSkills(syncPaths))
|
|
686
857
|
.filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
687
858
|
const previousState = withoutBaseSkillState(applyLegacyFirstRunState(
|
|
688
859
|
localSkills,
|
|
689
860
|
scopedState,
|
|
690
|
-
isEmptySyncState(scopedState)
|
|
861
|
+
!syncPaths.isolatedNamespace && isEmptySyncState(scopedState)
|
|
691
862
|
? (!previousAuthState || isEmptySyncState(previousAuthState)
|
|
692
863
|
? await deps.readLegacySyncState(syncPaths)
|
|
693
864
|
: previousAuthState)
|
|
694
865
|
: null,
|
|
695
866
|
));
|
|
867
|
+
pullResponse = bindNativeFolders(pullResponse, localSkills, previousState);
|
|
696
868
|
const gatheredSymlinkResult = await deps.syncSymlinks(
|
|
697
869
|
buildLocalSymlinkCandidates(pullResponse, localSkills, previousState),
|
|
698
870
|
syncPaths.skillsDir,
|
|
871
|
+
isolatedLinkOptions(syncPaths),
|
|
699
872
|
);
|
|
700
873
|
const pushCandidates = getPushCandidates(
|
|
701
874
|
localSkills,
|
|
702
875
|
previousState,
|
|
703
876
|
cloudCuratedSkillNames,
|
|
704
877
|
new Set(pullResponse.skills.map((skill) => skill.name)),
|
|
705
|
-
|
|
878
|
+
pullResponse.skills,
|
|
706
879
|
);
|
|
707
880
|
|
|
708
881
|
const failedPushes: SkillSyncFailure[] = [];
|
|
882
|
+
const acknowledgedNative = new Map<string, string>();
|
|
883
|
+
let writeLocalSkills = localSkills;
|
|
709
884
|
if (pushCandidates.length > 0) {
|
|
710
885
|
const pushResult = await deps.pushChangedSkills(serverUrl, jwt, pushCandidates);
|
|
886
|
+
for (const result of pushResult.skills || []) {
|
|
887
|
+
const saved = result as { id?: string; native_reference?: unknown };
|
|
888
|
+
const candidate = pushCandidates.find(local => local.nativeReference && local.cloudId === saved.id);
|
|
889
|
+
if (candidate && saved.native_reference) {
|
|
890
|
+
checkedNativeReference(saved.native_reference, candidate.cloudId);
|
|
891
|
+
acknowledgedNative.set(candidate.cloudId!, candidate.folderHash);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
711
894
|
if (Array.isArray(pushResult?.failed) && pushResult.failed.length > 0) {
|
|
712
895
|
failedPushes.push(...pushResult.failed);
|
|
713
896
|
console.warn(
|
|
@@ -717,6 +900,9 @@ export async function runSkillSync(
|
|
|
717
900
|
}
|
|
718
901
|
pullResponse = withoutBaseSkills(await deps.pullSkills(serverUrl, jwt));
|
|
719
902
|
assertSkillsPullAuthorized(pullResponse);
|
|
903
|
+
assertSyncEnvironment(pullResponse, syncSettings);
|
|
904
|
+
writeLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter(skill => !BASE_SKILL_NAMES.has(skill.name));
|
|
905
|
+
pullResponse = bindNativeFolders(pullResponse, writeLocalSkills, previousState);
|
|
720
906
|
}
|
|
721
907
|
|
|
722
908
|
// Delete phase: remove skills that were previously synced but are no longer in the cloud
|
|
@@ -724,8 +910,14 @@ export async function runSkillSync(
|
|
|
724
910
|
let deleted = 0;
|
|
725
911
|
for (const skillName of Object.keys(previousState.skills)) {
|
|
726
912
|
if (!cloudSkillNames.has(skillName)) {
|
|
913
|
+
const previous = previousState.skills[skillName];
|
|
914
|
+
if (previous.nativeReference || previous.nativeUnavailable
|
|
915
|
+
|| pullResponse.skills.some(skill => isNativeSkill(skill) && skill.id === previous.cloudId)) {
|
|
916
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
727
919
|
await deps.deleteLocalSkill(skillName, syncPaths);
|
|
728
|
-
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir);
|
|
920
|
+
await deps.removeAllSymlinksForSkill(skillName, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
729
921
|
deleted += 1;
|
|
730
922
|
}
|
|
731
923
|
}
|
|
@@ -735,13 +927,14 @@ export async function runSkillSync(
|
|
|
735
927
|
const appliedFilesByName: Record<string, string[]> = {};
|
|
736
928
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
737
929
|
pullResponse,
|
|
738
|
-
|
|
930
|
+
writeLocalSkills,
|
|
739
931
|
previousState,
|
|
740
932
|
syncPaths,
|
|
741
933
|
deps,
|
|
742
934
|
failedDownloads,
|
|
743
935
|
writtenSkillNames,
|
|
744
936
|
appliedFilesByName,
|
|
937
|
+
acknowledgedNative,
|
|
745
938
|
);
|
|
746
939
|
|
|
747
940
|
const finalLocalSkills = (await deps.scanLocalSkills(syncPaths))
|
|
@@ -749,6 +942,7 @@ export async function runSkillSync(
|
|
|
749
942
|
const symlinkResult = await deps.syncSymlinks(
|
|
750
943
|
buildLocalSymlinkCandidates(pullResponse, finalLocalSkills, previousState),
|
|
751
944
|
syncPaths.skillsDir,
|
|
945
|
+
isolatedLinkOptions(syncPaths),
|
|
752
946
|
);
|
|
753
947
|
const verifiedLinks = { ...(symlinkResult.verifiedAgentLinks ?? {}) };
|
|
754
948
|
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
@@ -763,6 +957,7 @@ export async function runSkillSync(
|
|
|
763
957
|
new Set(failedDownloads.map(item => item.name)),
|
|
764
958
|
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
765
959
|
collectAppliedFiles(pullResponse, previousState, appliedFilesByName),
|
|
960
|
+
previousState,
|
|
766
961
|
),
|
|
767
962
|
syncPaths,
|
|
768
963
|
);
|