@notis_ai/cli 0.2.0-beta.196.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 +11 -11
- package/config/portal-route-roots.json +0 -1
- package/dist/agent-hooks/notis-agent-hook.mjs +760 -473
- 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/skills/notis-cli/AGENT_INSTRUCTIONS.md +1 -1
- package/src/command-specs/auth.js +2 -2
- package/src/command-specs/handover.js +45 -16
- 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
|
@@ -84,9 +84,10 @@ from deploying an installed Space and requires the user's explicit authority.
|
|
|
84
84
|
Existing report rows render through `<ReportFrame>` in their database's view.
|
|
85
85
|
Use `notis-reports` for an independently authored report when appropriate.
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
Saved HTML pages and reports are notes in **Notis Notes**: save them with the
|
|
88
|
+
**Save HTML** Skill that comes with Notes, which owns the row-first
|
|
89
|
+
upload-and-attach workflow (see `notis-reports`). If Notes is not installed,
|
|
90
|
+
suggest installing it; creating an unrelated host is not a substitute.
|
|
90
91
|
|
|
91
92
|
[Context sharing](references/context.md) keeps selected text and annotations as
|
|
92
93
|
unsent reference context. Page context, memory hits and source content are not
|
|
@@ -34,7 +34,7 @@ for loading/error/empty states and explicit read caching.
|
|
|
34
34
|
|
|
35
35
|
| Component | Contract |
|
|
36
36
|
| --- | --- |
|
|
37
|
-
| `<DocumentPage recordKey>` | The full page of a record that is the view's main content (a note, a document): breadcrumb, Saving or Saved, '...' menu, cover, icon, title, meta strip, properties that save automatically, collaborative body. Slots `breadcrumb`, `actions`, `menuItems`, `aboveBody`, `belowBody`; toggles `share` (off by default), `header`, `meta`, `properties`, `width`; `onSaved`; `layout` to rearrange its parts. Pair with `DocumentPageSkeleton` and `usePrefetchRecord()`. Until the Notis CLI you build with exports it (`spaces build` replaces the Space's `packages/sdk` with the CLI's own SDK, and older ones have no `DocumentPage`, so the import fails the build), reach the same host page through `useNotisRuntime().ui?.DocumentPage` and fall back to `DocumentEditor` when it is missing, as the Notes Spaces do in `lib/document-page.tsx`. |
|
|
37
|
+
| `<DocumentPage recordKey>` | The full page of a record that is the view's main content (a note, a document): breadcrumb, Saving or Saved, '...' menu, cover, icon, title, meta strip, properties that save automatically, collaborative body. Slots `breadcrumb`, `actions`, `menuItems`, `aboveBody`, `belowBody`; toggles `share` (off by default, never offered for a report record), `header`, `meta`, `properties`, `width`; `viewer: 'html'` opens a record whose one stored `.html` file is the page (such as an HTML page saved in Notis Notes) full height under the toolbar, without header, meta, properties or body editor; `onSaved`; `layout` to rearrange its parts. Pair with `DocumentPageSkeleton` and `usePrefetchRecord()`. Until the Notis CLI you build with exports it (`spaces build` replaces the Space's `packages/sdk` with the CLI's own SDK, and older ones have no `DocumentPage`, so the import fails the build), reach the same host page through `useNotisRuntime().ui?.DocumentPage` and fall back to `DocumentEditor` when it is missing, as the Notes Spaces do in `lib/document-page.tsx`. |
|
|
38
38
|
| `<DocumentEditor recordKey>` | Native collaborative body, properties, images and attachments. With separate properties use `showProperties={false}`. |
|
|
39
39
|
| `<RecordProperties recordKey>` | Native revision-checked property editing. |
|
|
40
40
|
| `<HtmlFrame recordKey>` | Attached HTML under viewer CSP and an opaque-origin sandbox. Source cannot relax that boundary. |
|
|
@@ -44,6 +44,8 @@ for loading/error/empty states and explicit read caching.
|
|
|
44
44
|
The published Space declares the database binding; the server rechecks record
|
|
45
45
|
scope and binding revision. A Site exposes read-only body/property snapshots and
|
|
46
46
|
HTML for its pinned record, including a declared non-collection record param.
|
|
47
|
+
A report row (`<ReportFrame>`) cannot be shared as a Site; a report saved as an
|
|
48
|
+
HTML file can.
|
|
47
49
|
It has no upload, collaboration credential, authoring controls or sharing mutation.
|
|
48
50
|
Preview source does not gain live record editing. No app-owned credentials or
|
|
49
51
|
filesystem URLs belong in component props.
|
|
@@ -127,7 +127,7 @@ because a pending main view cannot yet open one.
|
|
|
127
127
|
## Rendering and memory
|
|
128
128
|
|
|
129
129
|
Choose [memory](memory.md) deliberately. `chrome: 'hidden'` is for full-screen
|
|
130
|
-
presentations
|
|
130
|
+
presentations; ordinary collection views keep Portal chrome.
|
|
131
131
|
A Space whose published view hides chrome starts hidden from each person's sidebar,
|
|
132
132
|
like Skill review Spaces; it stays reachable by link and search, and each person
|
|
133
133
|
can show it from the sidebar's hidden Spaces list or the Space's menu.
|
|
@@ -38,11 +38,13 @@ The CLI bundles this `notis-cli` base skill and refreshes its canonical copy und
|
|
|
38
38
|
|
|
39
39
|
A profile is one account paired with one API endpoint. Profiles live side by side; switching between them never signs any of them out.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- `npx --package @notis_ai/cli@latest -- notis
|
|
44
|
-
- `npx --package @notis_ai/cli@latest -- notis
|
|
45
|
-
- `npx --package @notis_ai/cli@latest -- notis
|
|
41
|
+
Notis Desktop signs the CLI in for every account saved in the app, one profile each. `default` keeps its name; each added account gets a profile named after its email with `@` turned into `-` (for example `flo-mindtheflo.com`), and Beta Desktop adds `-beta`. Switching accounts in Desktop never changes the active profile. Only the active profile's skills are linked into Claude Code, Codex and Cursor; every other account's skills sync into their own folder. Signing an account out of Desktop signs it out of the CLI on this computer and removes the profile and skills Desktop made for it. If that was the active profile, the CLI switches to the account Desktop opens next; with no other signed-in Desktop account it stays signed out. Profiles made with `notis login` are never removed or picked automatically.
|
|
42
|
+
|
|
43
|
+
- `npx --package @notis_ai/cli@latest -- notis profile list`: every profile on this machine, with its endpoint, user, and whether it is signed in. The active one is marked.
|
|
44
|
+
- `npx --package @notis_ai/cli@latest -- notis profile use <name>`: change which account subsequent commands run as, and link that account's skills into the agent folders straight away.
|
|
45
|
+
- `npx --package @notis_ai/cli@latest -- notis --profile <name> <command>`: run a single command as another account without changing the active one.
|
|
46
|
+
- `npx --package @notis_ai/cli@latest -- notis login --profile <name>`: add an account. The existing profiles keep their credentials.
|
|
47
|
+
- `npx --package @notis_ai/cli@latest -- notis logout`: sign this computer out of the active profile only; the account stays signed in on other computers. Pass `--all-profiles` to clear every profile on this computer.
|
|
46
48
|
|
|
47
49
|
Read the profile before acting on the user's data. `notis whoami` reports the account and endpoint a command will hit; if that is not the account the user meant, switch profiles rather than proceeding.
|
|
48
50
|
|
|
@@ -48,9 +48,10 @@ uncertain upload retries only the identical byte/intent request; it never replac
|
|
|
48
48
|
an existing shared retry object. Preserve previous files unless the user asked
|
|
49
49
|
for their removal.
|
|
50
50
|
|
|
51
|
-
For HTML pages, use the
|
|
52
|
-
|
|
53
|
-
absent. Deliver
|
|
51
|
+
For HTML pages and reports, use the Save HTML Skill that comes with Notis Notes,
|
|
52
|
+
which owns their complete save/update/sharing workflow (see `notis-reports`).
|
|
53
|
+
Suggest installing Notis Notes when absent. Deliver the note's Notes link, not the
|
|
54
|
+
raw file storage location.
|
|
54
55
|
|
|
55
56
|
## Fresh memory evidence
|
|
56
57
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useCallback, useRef, type CSSProperties, type ReactElement } from 'react';
|
|
4
4
|
import { useNotisRuntime } from '../provider';
|
|
5
5
|
import { DocumentEditor } from './DocumentEditor';
|
|
6
|
-
import { ShareControl } from './RecordComponents';
|
|
6
|
+
import { HtmlFrame, ShareControl } from './RecordComponents';
|
|
7
7
|
import { Skeleton } from './Skeleton';
|
|
8
8
|
import type { NotisDocumentPageProps } from '../runtime';
|
|
9
9
|
|
|
@@ -69,6 +69,7 @@ export function DocumentPageSkeleton({ width = 'standard', className, label }: {
|
|
|
69
69
|
* Shown by hosts that predate DocumentPage: the same record, with the editor these hosts already have.
|
|
70
70
|
* menuItems show as plain toolbar buttons and onSaved is called when a save finishes (title null);
|
|
71
71
|
* layout, header parts, meta and width options other than the column are not available there.
|
|
72
|
+
* With viewer 'html' the host's HtmlFrame replaces the editor.
|
|
72
73
|
*/
|
|
73
74
|
function DocumentPageFallback(props: NotisDocumentPageProps) {
|
|
74
75
|
const t = useCopy();
|
|
@@ -99,12 +100,13 @@ function DocumentPageFallback(props: NotisDocumentPageProps) {
|
|
|
99
100
|
style={{ background: 'none', border: 0, padding: '4px 8px', borderRadius: 6, cursor: item.disabled ? 'default' : 'pointer', font: 'inherit', fontSize: 13,
|
|
100
101
|
color: item.destructive ? 'hsl(var(--destructive))' : 'hsl(var(--muted-foreground))', opacity: item.disabled ? 0.5 : 1 }}>{item.label}</button>)}
|
|
101
102
|
</div> : null}
|
|
103
|
+
{props.viewer === 'html' ? <HtmlFrame recordKey={recordKey} /> :
|
|
102
104
|
<div style={{ ...columnStyle(width), padding: '24px 16px 96px', display: 'flex', flexDirection: 'column', gap: 20 }}>
|
|
103
105
|
{aboveBody}
|
|
104
106
|
<DocumentEditor recordKey={recordKey} variant={props.header === false ? 'body' : 'full'} showProperties={props.properties !== 'hidden'}
|
|
105
107
|
readOnly={readOnly} onDirtyChange={props.onDirtyChange} onSavingChange={savingChange} />
|
|
106
108
|
{belowBody}
|
|
107
|
-
</div>
|
|
109
|
+
</div>}
|
|
108
110
|
</div>;
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -114,7 +116,8 @@ function DocumentPageFallback(props: NotisDocumentPageProps) {
|
|
|
114
116
|
* automatically, and the collaborative body with presence. The host (Portal) draws it and keeps one
|
|
115
117
|
* save queue for the whole record. Files open in their viewer under the header; HTML and reports show
|
|
116
118
|
* only the toolbar (breadcrumb, actions, Share, '...' menu) above their frame, with no header, meta,
|
|
117
|
-
* properties, aboveBody or belowBody.
|
|
119
|
+
* properties, aboveBody or belowBody. `viewer: 'html'` opens a record whose one stored `.html` file is
|
|
120
|
+
* the page (for example a note that keeps a saved HTML report) the same way.
|
|
118
121
|
*
|
|
119
122
|
* ```tsx
|
|
120
123
|
* if (params.note) return <DocumentPage recordKey={params.note}
|
package/dist/sdk/src/runtime.ts
CHANGED
|
@@ -331,6 +331,11 @@ export interface NotisDocumentPageProps extends NotisRecordProps {
|
|
|
331
331
|
belowBody?: ReactNode;
|
|
332
332
|
/** Column width: standard (900px, as Beta), wide (1200px) or full. */
|
|
333
333
|
width?: 'standard' | 'wide' | 'full';
|
|
334
|
+
/**
|
|
335
|
+
* 'html': the record's one stored `.html` file is the page. It opens full height under the toolbar,
|
|
336
|
+
* like an HTML record, without the header, meta, properties or body editor.
|
|
337
|
+
*/
|
|
338
|
+
viewer?: 'html';
|
|
334
339
|
readOnly?: boolean;
|
|
335
340
|
onDirtyChange?: (dirty: boolean) => void;
|
|
336
341
|
onSavingChange?: (saving: boolean) => void;
|
|
@@ -442,8 +447,8 @@ export interface HandoverResult {
|
|
|
442
447
|
* The user's cloud computer, as far as an app may see it.
|
|
443
448
|
*
|
|
444
449
|
* `exists` is false when the user has never had a sandbox provisioned. A
|
|
445
|
-
* `status`
|
|
446
|
-
* these facts never wakes
|
|
450
|
+
* `status` is the last observed VM state; `freshness` and `observed_at` describe
|
|
451
|
+
* its age. Reading these facts never wakes the VM.
|
|
447
452
|
*/
|
|
448
453
|
export interface CloudComputerSandboxFacts {
|
|
449
454
|
exists: boolean;
|
|
@@ -451,6 +456,8 @@ export interface CloudComputerSandboxFacts {
|
|
|
451
456
|
provider: string | null;
|
|
452
457
|
created_at: string | null;
|
|
453
458
|
updated_at: string | null;
|
|
459
|
+
observed_at?: string | null;
|
|
460
|
+
freshness?: 'fresh' | 'stale' | 'unknown';
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
/**
|
|
@@ -465,6 +472,7 @@ export interface CloudComputerCliAuthFacts {
|
|
|
465
472
|
authenticated: boolean | null;
|
|
466
473
|
account: string | null;
|
|
467
474
|
checked_at: string | null;
|
|
475
|
+
freshness?: 'fresh' | 'stale' | 'unknown';
|
|
468
476
|
reason: string | null;
|
|
469
477
|
}
|
|
470
478
|
|
package/dist/skill-sync/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/runtime/skill-sync/index.ts
|
|
2
2
|
import { createHash as createHash4 } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { promises as fs3 } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
4
5
|
|
|
5
6
|
// src/runtime/skill-sync/types.ts
|
|
6
7
|
var DEFAULT_AGENT_TARGETS = {
|
|
@@ -113,8 +114,16 @@ function previousNativeState(state, skill) {
|
|
|
113
114
|
function nativeLocalReady(skill, local) {
|
|
114
115
|
return Boolean(local?.nativeFilesCurrent && local.folderHash === local.nativeBaseFolderHash && local.cloudId === skill.id && local.nativeScope === skill.native_scope && sameNativeReference(local.nativeReference, skill.native_reference));
|
|
115
116
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
117
|
+
function pendingLegacyUpgrade(skill, local, state) {
|
|
118
|
+
if (!isNativeSkill(skill) || !local || local.nativeReference || local.cloudId) return false;
|
|
119
|
+
const previous = previousNativeState(state, skill);
|
|
120
|
+
return Boolean(previous && !previous.nativeReference);
|
|
121
|
+
}
|
|
122
|
+
function nativeLinkRows(response, locals, state) {
|
|
123
|
+
return response.skills.map((skill) => {
|
|
124
|
+
const local = locals.find((row) => row.name === skill.name);
|
|
125
|
+
return isNativeSkill(skill) && !nativeLocalReady(skill, local) && !(state && pendingLegacyUpgrade(skill, local, state)) ? { ...skill, status: "disabled" } : skill;
|
|
126
|
+
});
|
|
118
127
|
}
|
|
119
128
|
function retainMissingNativeState(previous, next) {
|
|
120
129
|
const present = new Set(Object.values(next.skills).map((row) => row.cloudId));
|
|
@@ -896,7 +905,12 @@ async function replaceSkillDirectoryAtomically(skillDir, populateDir, preserve)
|
|
|
896
905
|
}
|
|
897
906
|
await populateDir(stagingDir);
|
|
898
907
|
if (preserve && await pathExists(skillDir)) {
|
|
899
|
-
await preserveUnmanagedFiles(
|
|
908
|
+
await preserveUnmanagedFiles(
|
|
909
|
+
skillDir,
|
|
910
|
+
stagingDir,
|
|
911
|
+
preserve.previouslyApplied,
|
|
912
|
+
preserve.native && !preserve.legacyBaseline
|
|
913
|
+
);
|
|
900
914
|
}
|
|
901
915
|
if (preserve?.native) {
|
|
902
916
|
const markerPath = path.join(stagingDir, NATIVE_IDENTITY_FILE);
|
|
@@ -993,7 +1007,9 @@ async function writeCloudSkillToDisk(skill, bundleBytes, paths = DEFAULT_SYNC_PA
|
|
|
993
1007
|
const preserve = {
|
|
994
1008
|
previouslyApplied: options.previouslyApplied,
|
|
995
1009
|
expectedFolderHash: options.expectedFolderHash,
|
|
996
|
-
native: isNativeSkill(skill)
|
|
1010
|
+
native: isNativeSkill(skill),
|
|
1011
|
+
// An unchanged legacy folder upgrading in place: shared files take the cloud version.
|
|
1012
|
+
legacyBaseline: isNativeSkill(skill) && options.legacyBaseline === true && options.expectedFolderHash !== void 0
|
|
997
1013
|
};
|
|
998
1014
|
if (bundleBytes?.length) {
|
|
999
1015
|
const bundlePath = path.join(
|
|
@@ -1560,6 +1576,44 @@ var DEFAULT_RUN_SKILL_SYNC_DEPS = {
|
|
|
1560
1576
|
removeForeignAccountSymlinks,
|
|
1561
1577
|
updateAgentTargets
|
|
1562
1578
|
};
|
|
1579
|
+
var MIRROR_ONLY_DEPS = {
|
|
1580
|
+
removeForeignAccountSymlinks: async () => 0,
|
|
1581
|
+
removeAllSymlinksForSkill: async () => 0,
|
|
1582
|
+
syncSymlinks: async () => ({ linked: 0, removed: 0, skipped: 0, verifiedAgentLinks: {} })
|
|
1583
|
+
};
|
|
1584
|
+
async function forgetOtherAccountsAgentLinks(paths) {
|
|
1585
|
+
const usersDir = dirname(dirname(paths.syncStatePath));
|
|
1586
|
+
let entries;
|
|
1587
|
+
try {
|
|
1588
|
+
entries = await fs3.readdir(usersDir, { withFileTypes: true });
|
|
1589
|
+
} catch {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
for (const entry of entries) {
|
|
1593
|
+
if (!entry.isDirectory()) continue;
|
|
1594
|
+
const statePath = join(usersDir, entry.name, ".notis-sync.json");
|
|
1595
|
+
if (statePath === paths.syncStatePath) continue;
|
|
1596
|
+
let state = null;
|
|
1597
|
+
try {
|
|
1598
|
+
state = JSON.parse(await fs3.readFile(statePath, "utf8"));
|
|
1599
|
+
} catch {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (!state || typeof state !== "object" || !state.skills || typeof state.skills !== "object") continue;
|
|
1603
|
+
let changed = false;
|
|
1604
|
+
for (const skill of Object.values(state.skills)) {
|
|
1605
|
+
if (skill && typeof skill === "object" && "verifiedAgentLinks" in skill) {
|
|
1606
|
+
delete skill.verifiedAgentLinks;
|
|
1607
|
+
changed = true;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (!changed) continue;
|
|
1611
|
+
const temporaryPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1612
|
+
await fs3.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
|
|
1613
|
+
`, "utf8");
|
|
1614
|
+
await fs3.rename(temporaryPath, statePath);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1563
1617
|
function isolatedLinkOptions(paths) {
|
|
1564
1618
|
if (!paths.isolatedNamespace && !paths.homeDir) return void 0;
|
|
1565
1619
|
const root = paths.isolatedNamespace ? join(paths.syncRoot, "environments", paths.isolatedNamespace, "agents") : paths.homeDir;
|
|
@@ -1647,6 +1701,10 @@ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLi
|
|
|
1647
1701
|
const localSkill = localSkillMap.get(skill.name);
|
|
1648
1702
|
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
1649
1703
|
const appliedFileList = appliedFiles[skill.name];
|
|
1704
|
+
if (pendingLegacyUpgrade(skill, localSkill, previousState)) {
|
|
1705
|
+
const { nativeUnavailable: _unavailable, ...previous } = previousNativeState(previousState, skill);
|
|
1706
|
+
return [skill.name, { ...previous, verifiedAgentLinks: verifiedAgentLinks[skill.name] ?? {} }];
|
|
1707
|
+
}
|
|
1650
1708
|
if (isNativeSkill(skill) && !nativeLocalReady(skill, localSkill)) {
|
|
1651
1709
|
const previous = previousNativeState(previousState, skill);
|
|
1652
1710
|
return [skill.name, { ...previous || {
|
|
@@ -1710,7 +1768,7 @@ function buildLocalSymlinkCandidates(pullResponse, localSkills, previousState) {
|
|
|
1710
1768
|
status: "active"
|
|
1711
1769
|
};
|
|
1712
1770
|
});
|
|
1713
|
-
return [...nativeLinkRows(pullResponse, localSkills), ...localOnlySkills];
|
|
1771
|
+
return [...nativeLinkRows(pullResponse, localSkills, previousState), ...localOnlySkills];
|
|
1714
1772
|
}
|
|
1715
1773
|
function isEmptySyncState(state) {
|
|
1716
1774
|
return state.lastSyncedAt === null && Object.keys(state.skills).length === 0;
|
|
@@ -1746,6 +1804,7 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
|
|
|
1746
1804
|
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
1747
1805
|
const local = localSkillMap.get(cloudSkill.name);
|
|
1748
1806
|
const acknowledged = isNativeSkill(cloudSkill) && local && acknowledgedNative.get(cloudSkill.id) === local.folderHash;
|
|
1807
|
+
const legacyBaseline = Boolean(local && pendingLegacyUpgrade(cloudSkill, local, previousState) && previousNativeState(previousState, cloudSkill)?.folderHash === local.folderHash);
|
|
1749
1808
|
if (!acknowledged && !shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
1750
1809
|
if (isNativeSkill(cloudSkill) && !nativeLocalReady(cloudSkill, local)) {
|
|
1751
1810
|
failures.push({ name: cloudSkill.name, error: "Local native Skill changes were retained; resolve the version or identity conflict before using them." });
|
|
@@ -1758,7 +1817,8 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
|
|
|
1758
1817
|
// What the last write put there. Everything else in the folder was
|
|
1759
1818
|
// produced locally and survives this one.
|
|
1760
1819
|
previouslyApplied: previousNativeState(previousState, skill)?.appliedFiles,
|
|
1761
|
-
...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {}
|
|
1820
|
+
...isNativeSkill(skill) && local ? { expectedFolderHash: local.folderHash } : {},
|
|
1821
|
+
...legacyBaseline ? { legacyBaseline } : {}
|
|
1762
1822
|
}),
|
|
1763
1823
|
onWarning: warnSkillSync
|
|
1764
1824
|
});
|
|
@@ -1827,7 +1887,7 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
|
|
|
1827
1887
|
}
|
|
1828
1888
|
if (relinkSkillNames.size > 0) {
|
|
1829
1889
|
const relinked = await deps.syncSymlinks(
|
|
1830
|
-
nativeLinkRows(pullResponse, finalLocalSkills).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
1890
|
+
nativeLinkRows(pullResponse, finalLocalSkills, previousState).filter((skill) => relinkSkillNames.has(skill.name)),
|
|
1831
1891
|
syncPaths.skillsDir,
|
|
1832
1892
|
{ ...isolatedLinkOptions(syncPaths), removeUndesired: false }
|
|
1833
1893
|
);
|
|
@@ -1864,7 +1924,8 @@ async function materializeCloudSkillsForLocalShell(serverUrl, jwt, dependencies
|
|
|
1864
1924
|
for (const [name, previous] of Object.entries(previousState.skills)) {
|
|
1865
1925
|
if (!previous.nativeReference && !previous.nativeUnavailable) continue;
|
|
1866
1926
|
const current = pullResponse.skills.find((skill) => skill.id === previous.cloudId);
|
|
1867
|
-
|
|
1927
|
+
const local = current && finalLocalSkills.find((row) => row.name === current.name);
|
|
1928
|
+
if (!current || !nativeLocalReady(current, local) && !pendingLegacyUpgrade(current, local, previousState)) {
|
|
1868
1929
|
removed += await deps.removeAllSymlinksForSkill(name, syncPaths.skillsDir, isolatedLinkOptions(syncPaths));
|
|
1869
1930
|
}
|
|
1870
1931
|
}
|
|
@@ -1950,9 +2011,11 @@ async function deactivateDeletedAgentSkills(serverUrl, jwt, pullResponse, previo
|
|
|
1950
2011
|
return deactivated;
|
|
1951
2012
|
}
|
|
1952
2013
|
async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
2014
|
+
const linkAgentFolders = options.linkAgentFolders !== false;
|
|
1953
2015
|
const deps = {
|
|
1954
2016
|
...DEFAULT_RUN_SKILL_SYNC_DEPS,
|
|
1955
|
-
...dependencies
|
|
2017
|
+
...dependencies,
|
|
2018
|
+
...linkAgentFolders ? {} : MIRROR_ONLY_DEPS
|
|
1956
2019
|
};
|
|
1957
2020
|
const syncSettings = await deps.fetchSyncSettings(
|
|
1958
2021
|
serverUrl,
|
|
@@ -1991,13 +2054,16 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
1991
2054
|
syncPaths.skillsDir,
|
|
1992
2055
|
isolatedLinkOptions(syncPaths)
|
|
1993
2056
|
);
|
|
2057
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
2058
|
+
await forgetOtherAccountsAgentLinks(syncPaths);
|
|
2059
|
+
}
|
|
1994
2060
|
const cloudCuratedSkillNames = new Set(
|
|
1995
2061
|
pullResponse.skills.filter((skill) => skill.source === "curated").map((skill) => skill.name)
|
|
1996
2062
|
);
|
|
1997
2063
|
const protectedSkillNames = /* @__PURE__ */ new Set([...cloudCuratedSkillNames, ...BASE_SKILL_NAMES]);
|
|
1998
2064
|
const scopedState = withoutBaseSkillState(await deps.readSyncState(syncPaths));
|
|
1999
2065
|
const assignmentFailures = [];
|
|
2000
|
-
const deactivated = !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
|
|
2066
|
+
const deactivated = linkAgentFolders && !syncPaths.isolatedNamespace && syncSettings.agent_targets_conditional_updates === true ? await deactivateDeletedAgentSkills(
|
|
2001
2067
|
serverUrl,
|
|
2002
2068
|
jwt,
|
|
2003
2069
|
pullResponse,
|
|
@@ -2019,7 +2085,7 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2019
2085
|
protectedSkillNames
|
|
2020
2086
|
});
|
|
2021
2087
|
}
|
|
2022
|
-
if (!syncPaths.isolatedNamespace) {
|
|
2088
|
+
if (linkAgentFolders && !syncPaths.isolatedNamespace) {
|
|
2023
2089
|
await deps.gatherTopLevelLocalSkills(syncPaths, { protectedSkillNames });
|
|
2024
2090
|
}
|
|
2025
2091
|
const localSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|