@notis_ai/cli 0.2.0-beta.197.1 → 0.2.0-beta.198.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 +5 -5
- package/dist/agent-hooks/notis-agent-hook.mjs +729 -460
- package/dist/base-skills/notis-apps/SKILL.md +4 -3
- package/dist/base-skills/notis-apps/references/sdk.md +3 -1
- package/dist/base-skills/notis-apps/references/views.md +1 -1
- package/dist/base-skills/notis-cli/SKILL.md +7 -5
- package/dist/base-skills/notis-query/references/documents.md +4 -3
- package/dist/sdk/src/components/DocumentPage.tsx +6 -3
- package/dist/sdk/src/runtime.ts +10 -2
- package/dist/skill-sync/index.js +78 -12
- package/dist/skill-sync/index.js.map +3 -3
- package/dist/skill-sync-worker.mjs +391 -175
- package/dist/view-renderer/dist/host.js +86 -86
- package/dist/view-renderer/src/host.tsx +4 -2
- package/package.json +1 -1
- package/skills/notis-apps/cli.md +1 -1
- package/src/command-specs/auth.js +2 -2
- package/src/command-specs/profile.js +42 -1
- package/src/command-specs/skills.js +13 -1
- 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/oauth.js +18 -1
- package/src/runtime/profiles.js +5 -107
- package/src/runtime/skill-sync/index.ts +83 -7
- package/src/runtime/skill-sync/local-scanner.ts +7 -4
- package/src/runtime/skill-sync/native-plan.ts +14 -3
- package/src/runtime/sync-skills.d.ts +3 -1
- package/src/runtime/sync-skills.js +10 -2
- package/src/skill-sync-worker.js +14 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
3
4
|
import type {
|
|
4
5
|
AgentTargets,
|
|
5
6
|
LocalSkill,
|
|
@@ -11,7 +12,7 @@ import type {
|
|
|
11
12
|
} from "./types";
|
|
12
13
|
import { normalizeAgentTargets } from "./types";
|
|
13
14
|
import { assertNativePull, checkedNativeReference, isNativeSkill, nativeState } from './native-identity';
|
|
14
|
-
import { bindNativeFolders, hasNativeFolderHistory, nativeLinkRows, nativeLocalReady, previousNativeState, retainMissingNativeState } from './native-plan';
|
|
15
|
+
import { bindNativeFolders, hasNativeFolderHistory, nativeLinkRows, nativeLocalReady, pendingLegacyUpgrade, previousNativeState, retainMissingNativeState } from './native-plan';
|
|
15
16
|
import {
|
|
16
17
|
downloadSkillBundle,
|
|
17
18
|
fetchSyncSettings,
|
|
@@ -65,6 +66,10 @@ export interface RunSkillSyncOptions {
|
|
|
65
66
|
honorSyncEnabled?: boolean;
|
|
66
67
|
/** All account paths, scanner roots and agent links use this explicit home. */
|
|
67
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;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
const BASE_SKILL_NAMES = new Set(['notis-apps', 'notis-query', 'notis-cli']);
|
|
@@ -143,6 +148,58 @@ const DEFAULT_RUN_SKILL_SYNC_DEPS: RunSkillSyncDependencies = {
|
|
|
143
148
|
updateAgentTargets,
|
|
144
149
|
};
|
|
145
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
|
+
|
|
146
203
|
export function isolatedLinkOptions(paths: ReturnType<typeof getSkillSyncPathsForUser>) {
|
|
147
204
|
if (!paths.isolatedNamespace && !paths.homeDir) return undefined;
|
|
148
205
|
const root = paths.isolatedNamespace
|
|
@@ -299,6 +356,11 @@ function buildSyncState(
|
|
|
299
356
|
const localSkill = localSkillMap.get(skill.name);
|
|
300
357
|
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
301
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
|
+
}
|
|
302
364
|
if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
|
|
303
365
|
const previous = previousNativeState(previousState, skill);
|
|
304
366
|
return [skill.name, { ...(previous || {
|
|
@@ -368,7 +430,7 @@ function buildLocalSymlinkCandidates(
|
|
|
368
430
|
};
|
|
369
431
|
});
|
|
370
432
|
|
|
371
|
-
return [...nativeLinkRows(pullResponse, localSkills), ...localOnlySkills];
|
|
433
|
+
return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
|
|
372
434
|
}
|
|
373
435
|
|
|
374
436
|
function isEmptySyncState(state: NotisSyncState): boolean {
|
|
@@ -428,6 +490,10 @@ async function writePulledSkillsToScopedMirror(
|
|
|
428
490
|
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
429
491
|
const local = localSkillMap.get(cloudSkill.name);
|
|
430
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);
|
|
431
497
|
if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
432
498
|
if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
|
|
433
499
|
failures.push({ name: cloudSkill.name, error: 'Local native Skill changes were retained; resolve the version or identity conflict before using them.' });
|
|
@@ -443,6 +509,7 @@ async function writePulledSkillsToScopedMirror(
|
|
|
443
509
|
// produced locally and survives this one.
|
|
444
510
|
previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
|
|
445
511
|
...(isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}),
|
|
512
|
+
...(legacyBaseline ? { legacyBaseline } : {}),
|
|
446
513
|
}),
|
|
447
514
|
onWarning: warnSkillSync,
|
|
448
515
|
});
|
|
@@ -542,7 +609,7 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
542
609
|
}
|
|
543
610
|
if (relinkSkillNames.size > 0) {
|
|
544
611
|
const relinked = await deps.syncSymlinks(
|
|
545
|
-
nativeLinkRows(pullResponse, finalLocalSkills).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
612
|
+
nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
546
613
|
syncPaths.skillsDir,
|
|
547
614
|
{ ...isolatedLinkOptions(syncPaths), removeUndesired: false },
|
|
548
615
|
);
|
|
@@ -580,7 +647,8 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
580
647
|
for (const [name, previous] of Object.entries(previousState.skills)) {
|
|
581
648
|
if (!previous.nativeReference && !previous.nativeUnavailable) continue;
|
|
582
649
|
const current = pullResponse.skills.find(skill => skill.id === previous.cloudId);
|
|
583
|
-
|
|
650
|
+
const local = current && finalLocalSkills.find(row => row.name === current.name);
|
|
651
|
+
if (!current || (!nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState))) {
|
|
584
652
|
removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
585
653
|
}
|
|
586
654
|
}
|
|
@@ -702,9 +770,11 @@ export async function runSkillSync(
|
|
|
702
770
|
dependencies: Partial<RunSkillSyncDependencies> = {},
|
|
703
771
|
options: RunSkillSyncOptions = {},
|
|
704
772
|
): Promise<RunSkillSyncResult> {
|
|
773
|
+
const linkAgentFolders = options.linkAgentFolders !== false;
|
|
705
774
|
const deps = {
|
|
706
775
|
...DEFAULT_RUN_SKILL_SYNC_DEPS,
|
|
707
776
|
...dependencies,
|
|
777
|
+
...(linkAgentFolders ? {} : MIRROR_ONLY_DEPS),
|
|
708
778
|
};
|
|
709
779
|
const syncSettings: SyncSettings = await deps.fetchSyncSettings(
|
|
710
780
|
serverUrl,
|
|
@@ -749,6 +819,9 @@ export async function runSkillSync(
|
|
|
749
819
|
const foreignLinksRemoved = await deps.removeForeignAccountSymlinks(
|
|
750
820
|
syncPaths.skillsDir, isolatedLinkOptions(syncPaths),
|
|
751
821
|
);
|
|
822
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
823
|
+
await forgetOtherAccountsAgentLinks(syncPaths);
|
|
824
|
+
}
|
|
752
825
|
|
|
753
826
|
const cloudCuratedSkillNames = new Set(
|
|
754
827
|
pullResponse.skills
|
|
@@ -758,7 +831,9 @@ export async function runSkillSync(
|
|
|
758
831
|
const protectedSkillNames = new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
|
|
759
832
|
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
760
833
|
const assignmentFailures: SkillSyncFailure[] = [];
|
|
761
|
-
|
|
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
|
|
762
837
|
? await deactivateDeletedAgentSkills(
|
|
763
838
|
serverUrl, jwt, pullResponse, scopedState, scopedState, syncPaths.skillsDir, deps, assignmentFailures, isolatedLinkOptions(syncPaths),
|
|
764
839
|
)
|
|
@@ -774,7 +849,8 @@ export async function runSkillSync(
|
|
|
774
849
|
protectedSkillNames,
|
|
775
850
|
});
|
|
776
851
|
}
|
|
777
|
-
if (!syncPaths.isolatedNamespace) {
|
|
852
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
853
|
+
// Skills found loose in the agent folders belong to the account linking them.
|
|
778
854
|
await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
|
|
779
855
|
}
|
|
780
856
|
const localSkills = (await deps.scanLocalSkills(syncPaths))
|
|
@@ -1052,7 +1052,7 @@ async function pathExists(targetPath: string): Promise<boolean> {
|
|
|
1052
1052
|
async function replaceSkillDirectoryAtomically(
|
|
1053
1053
|
skillDir: string,
|
|
1054
1054
|
populateDir: (stagingDir: string) => Promise<void>,
|
|
1055
|
-
preserve?: { previouslyApplied?: readonly string[]; expectedFolderHash?: string; native?: boolean },
|
|
1055
|
+
preserve?: { previouslyApplied?: readonly string[]; expectedFolderHash?: string; native?: boolean; legacyBaseline?: boolean },
|
|
1056
1056
|
): Promise<void> {
|
|
1057
1057
|
const parentDir = path.dirname(skillDir);
|
|
1058
1058
|
const skillName = path.basename(skillDir);
|
|
@@ -1084,7 +1084,8 @@ async function replaceSkillDirectoryAtomically(
|
|
|
1084
1084
|
await populateDir(stagingDir);
|
|
1085
1085
|
|
|
1086
1086
|
if (preserve && (await pathExists(skillDir))) {
|
|
1087
|
-
await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied,
|
|
1087
|
+
await preserveUnmanagedFiles(skillDir, stagingDir, preserve.previouslyApplied,
|
|
1088
|
+
preserve.native && !preserve.legacyBaseline);
|
|
1088
1089
|
}
|
|
1089
1090
|
|
|
1090
1091
|
if (preserve?.native) {
|
|
@@ -1178,7 +1179,7 @@ export async function writeCloudSkillToDisk(
|
|
|
1178
1179
|
skill: CloudSkill,
|
|
1179
1180
|
bundleBytes?: Buffer,
|
|
1180
1181
|
paths: SkillSyncPaths = DEFAULT_SYNC_PATHS,
|
|
1181
|
-
options: { previouslyApplied?: readonly string[]; expectedFolderHash?: string } = {},
|
|
1182
|
+
options: { previouslyApplied?: readonly string[]; expectedFolderHash?: string; legacyBaseline?: boolean } = {},
|
|
1182
1183
|
): Promise<SkillWriteOutcome | false> {
|
|
1183
1184
|
if (isNativeSkill(skill)) {
|
|
1184
1185
|
assertNativePull({ skills: [{ ...skill, name: 'native-' + skill.id }], skill_sync_protocol: 2, sync_scope: skill.native_scope,
|
|
@@ -1190,7 +1191,9 @@ export async function writeCloudSkillToDisk(
|
|
|
1190
1191
|
safeName(skill.name, paths.skillsDir),
|
|
1191
1192
|
);
|
|
1192
1193
|
const preserve = { previouslyApplied: options.previouslyApplied, expectedFolderHash: options.expectedFolderHash,
|
|
1193
|
-
native: isNativeSkill(skill)
|
|
1194
|
+
native: isNativeSkill(skill),
|
|
1195
|
+
// An unchanged legacy folder upgrading in place: shared files take the cloud version.
|
|
1196
|
+
legacyBaseline: isNativeSkill(skill) && options.legacyBaseline === true && options.expectedFolderHash !== undefined };
|
|
1194
1197
|
|
|
1195
1198
|
if (bundleBytes?.length) {
|
|
1196
1199
|
const bundlePath = path.join(
|
|
@@ -34,9 +34,20 @@ export function nativeLocalReady(skill: CloudSkill, local?: LocalSkill): boolean
|
|
|
34
34
|
&& sameNativeReference(local.nativeReference, skill.native_reference));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
/** A pre-native folder (no identity marker, no native state) the cloud now serves
|
|
38
|
+
* as a native Skill. It stays usable as before until its upgrade write lands. */
|
|
39
|
+
export function pendingLegacyUpgrade(skill: CloudSkill, local: LocalSkill | undefined, state: NotisSyncState): boolean {
|
|
40
|
+
if (!isNativeSkill(skill) || !local || local.nativeReference || local.cloudId) return false;
|
|
41
|
+
const previous = previousNativeState(state, skill);
|
|
42
|
+
return Boolean(previous && !previous.nativeReference);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function nativeLinkRows(response: SyncPullResponse, locals: LocalSkill[], state?: NotisSyncState): CloudSkill[] {
|
|
46
|
+
return response.skills.map(skill => {
|
|
47
|
+
const local = locals.find(row => row.name === skill.name);
|
|
48
|
+
return isNativeSkill(skill) && !nativeLocalReady(skill, local) && !(state && pendingLegacyUpgrade(skill, local, state))
|
|
49
|
+
? { ...skill, status: 'disabled' } : skill;
|
|
50
|
+
});
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
export function retainMissingNativeState(previous: NotisSyncState, next: NotisSyncState): NotisSyncState {
|
|
@@ -30,8 +30,10 @@ export function reconcileAllSkills<T extends object>(options: {
|
|
|
30
30
|
serverUrl: string,
|
|
31
31
|
jwt: string,
|
|
32
32
|
dependencies: Record<string, never>,
|
|
33
|
-
options: { honorSyncEnabled: boolean },
|
|
33
|
+
options: { honorSyncEnabled: boolean; syncHome?: string; linkAgentFolders?: boolean },
|
|
34
34
|
) => Promise<T>;
|
|
35
35
|
reconcileBase?: (options: { userId?: string | null; home?: string }) => BaseSkillReconcileResult;
|
|
36
36
|
lockOptions?: Omit<SkillSyncLockOptions, 'home'>;
|
|
37
|
+
/** False syncs the account's own folder only; another account owns the agent folders. */
|
|
38
|
+
linkAgentFolders?: boolean;
|
|
37
39
|
}): Promise<T & ReconcileAllSkillsResult>;
|
|
@@ -211,14 +211,22 @@ export async function reconcileAllSkills({
|
|
|
211
211
|
runAccountSync,
|
|
212
212
|
reconcileBase = reconcileBaseSkills,
|
|
213
213
|
lockOptions = {},
|
|
214
|
+
linkAgentFolders = true,
|
|
214
215
|
}) {
|
|
215
216
|
return withSkillSyncLock(async () => {
|
|
216
|
-
|
|
217
|
+
// Base skills are linked from the account that owns the agent folders.
|
|
218
|
+
const base = linkAgentFolders
|
|
219
|
+
? reconcileBase({ userId, ...(home ? { home } : {}) })
|
|
220
|
+
: { skills: [], installed: 0, linked: 0, unchanged: 0, backups: [] };
|
|
217
221
|
const account = await runAccountSync(
|
|
218
222
|
serverUrl,
|
|
219
223
|
jwt,
|
|
220
224
|
{},
|
|
221
|
-
{
|
|
225
|
+
{
|
|
226
|
+
honorSyncEnabled,
|
|
227
|
+
...(home ? { syncHome: home } : {}),
|
|
228
|
+
...(linkAgentFolders ? {} : { linkAgentFolders: false }),
|
|
229
|
+
},
|
|
222
230
|
);
|
|
223
231
|
return {
|
|
224
232
|
...account,
|
package/src/skill-sync-worker.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { resolveRuntimeProfile } from './runtime/profiles.js';
|
|
4
|
+
import { getCurrentProfileName, loadConfig, resolveRuntimeProfile } from './runtime/profiles.js';
|
|
5
5
|
import { ensureFreshOAuthCredential } from './runtime/oauth.js';
|
|
6
6
|
import { runSkillSync, fetchSyncSettings } from '../dist/skill-sync/index.js';
|
|
7
7
|
import { withSkillSyncLock } from './runtime/sync-skills.js';
|
|
8
8
|
|
|
9
|
+
function currentCliProfile() {
|
|
10
|
+
try {
|
|
11
|
+
return getCurrentProfileName(loadConfig());
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export async function runAutomaticSkillSync({ profile, apiBase, userId }, {
|
|
10
18
|
resolveRuntime = resolveRuntimeProfile, refresh = ensureFreshOAuthCredential,
|
|
11
19
|
settings = fetchSyncSettings, sync = runSkillSync, lock = withSkillSyncLock,
|
|
20
|
+
currentProfile = currentCliProfile,
|
|
12
21
|
} = {}) {
|
|
13
22
|
const runtime = resolveRuntime({ profile }, { requireAuth: true });
|
|
14
23
|
if (runtime.credentialKind !== 'oauth' || runtime.apiBase !== apiBase
|
|
@@ -18,9 +27,12 @@ export async function runAutomaticSkillSync({ profile, apiBase, userId }, {
|
|
|
18
27
|
if (saved.user_id !== userId) throw new Error('Automatic skill sync identity mismatch');
|
|
19
28
|
// Do not gather, unlink foreign accounts or write anything when opted out.
|
|
20
29
|
if (!saved.sync_enabled) return { status: 'disabled' };
|
|
30
|
+
// The service stays bound to its profile; after `notis profile use` moves the
|
|
31
|
+
// default away, it keeps that account's folder in sync without linking it.
|
|
32
|
+
const linkAgentFolders = (currentProfile() ?? profile) === profile;
|
|
21
33
|
const result = await lock(() => sync(runtime.apiBase, runtime.jwt, {
|
|
22
34
|
fetchSyncSettings: async () => saved,
|
|
23
|
-
}, { honorSyncEnabled: true }));
|
|
35
|
+
}, { honorSyncEnabled: true, ...(linkAgentFolders ? {} : { linkAgentFolders: false }) }));
|
|
24
36
|
return { status: (result.failedLinks?.length || result.failedPushes?.length) ? 'partial' : 'synced', ...result };
|
|
25
37
|
}
|
|
26
38
|
|