@notis_ai/cli 0.2.0-beta.161.1 → 0.2.0-beta.164.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/dist/agent-hooks/notis-agent-hook.mjs +47 -11
- package/dist/base-skills/notis-cli/SKILL.md +0 -1
- package/dist/skill-sync/index.js +44 -8
- package/dist/skill-sync/index.js.map +2 -2
- package/dist/skill-sync-worker.mjs +42 -7
- package/package.json +1 -1
- package/src/runtime/app-platform.js +6 -6
- package/src/runtime/oauth.js +2 -2
- package/src/runtime/skill-sync/index.ts +59 -4
- package/src/runtime/skill-sync/sync-plan.ts +26 -1
- package/src/runtime/skill-sync/types.ts +8 -0
- package/template/.harness/index.html.tmpl +1 -1
|
@@ -2365,11 +2365,24 @@ async function syncSymlinks(skills, skillsDir = LEGACY_AGENTS_SKILLS_DIR, option
|
|
|
2365
2365
|
}
|
|
2366
2366
|
return result;
|
|
2367
2367
|
}
|
|
2368
|
-
function
|
|
2368
|
+
function selectCloudSkillsToApply(skills) {
|
|
2369
|
+
const winners = /* @__PURE__ */ new Map();
|
|
2370
|
+
for (const skill of skills) {
|
|
2371
|
+
const current = winners.get(skill.name);
|
|
2372
|
+
if (!current || (skill.updated_at || "") >= (current.updated_at || "")) {
|
|
2373
|
+
winners.set(skill.name, skill);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
return [...winners.values()];
|
|
2377
|
+
}
|
|
2378
|
+
function getPushCandidates(localSkills, syncState, cloudCuratedSkillNames = /* @__PURE__ */ new Set(), cloudSkillNames, cloudAppOwnedSkillNames = /* @__PURE__ */ new Set()) {
|
|
2369
2379
|
return localSkills.filter((skill) => {
|
|
2370
2380
|
if (cloudCuratedSkillNames.has(skill.name)) {
|
|
2371
2381
|
return false;
|
|
2372
2382
|
}
|
|
2383
|
+
if (cloudAppOwnedSkillNames.has(skill.name)) {
|
|
2384
|
+
return false;
|
|
2385
|
+
}
|
|
2373
2386
|
const previous = syncState.skills[skill.name];
|
|
2374
2387
|
if (!previous) {
|
|
2375
2388
|
return true;
|
|
@@ -2482,17 +2495,28 @@ function shouldWriteCloudSkill(cloudSkill, localSkills, previousState) {
|
|
|
2482
2495
|
}
|
|
2483
2496
|
const previous = previousState.skills[skillName];
|
|
2484
2497
|
if (previous?.folderHash === localSkill.folderHash && previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
|
|
2498
|
+
if (previous && previous.cloudContentHash === void 0 && previous.cloudId === cloudSkill.id && previous.appliedCloudFolderHash !== void 0 && previous.appliedCloudFolderHash === cloudHash && previous.cloudUpdatedAt === cloudSkill.updated_at && previous.folderHash === localSkill.folderHash) {
|
|
2499
|
+
return false;
|
|
2500
|
+
}
|
|
2485
2501
|
if (cloudSkill.source === "curated") {
|
|
2486
2502
|
return cloudHash ? cloudHash !== localSkill.folderHash : true;
|
|
2487
2503
|
}
|
|
2488
2504
|
const localChangedSinceLastSync = !previous || previous.folderHash !== localSkill.folderHash;
|
|
2489
2505
|
return !localChangedSinceLastSync && (Boolean(cloudHash) && cloudHash !== localSkill.folderHash || Boolean(previous?.cloudContentHash && previous.cloudContentHash !== cloudContentHash(cloudSkill)));
|
|
2490
2506
|
}
|
|
2491
|
-
function
|
|
2507
|
+
function collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames) {
|
|
2508
|
+
const applied = {};
|
|
2509
|
+
for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
2510
|
+
applied[skill.name] = writtenSkillNames.has(skill.name) ? skill.skill_folder_hash || "" : previousState.skills[skill.name]?.appliedCloudFolderHash;
|
|
2511
|
+
}
|
|
2512
|
+
return applied;
|
|
2513
|
+
}
|
|
2514
|
+
function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLinks = {}, failedContentNames = /* @__PURE__ */ new Set(), appliedRevisions = {}) {
|
|
2492
2515
|
const localSkillMap = toSkillMap(localSkills);
|
|
2493
2516
|
const skills = Object.fromEntries(
|
|
2494
|
-
pullResponse.skills.map((skill) => {
|
|
2517
|
+
selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
|
|
2495
2518
|
const localSkill = localSkillMap.get(skill.name);
|
|
2519
|
+
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
2496
2520
|
return [
|
|
2497
2521
|
skill.name,
|
|
2498
2522
|
{
|
|
@@ -2502,6 +2526,7 @@ function buildSyncState(pullResponse, localSkills, lastSyncedAt, verifiedAgentLi
|
|
|
2502
2526
|
verifiedAgentLinks: skill.status === "active" ? verifiedAgentLinks[skill.name] ?? {} : {},
|
|
2503
2527
|
cloudUpdatedAt: skill.updated_at,
|
|
2504
2528
|
...!failedContentNames.has(skill.name) && !skill.skill_source_url ? { cloudContentHash: cloudContentHash(skill) } : {},
|
|
2529
|
+
...appliedCloudFolderHash !== void 0 ? { appliedCloudFolderHash } : {},
|
|
2505
2530
|
syncedAt: lastSyncedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
2506
2531
|
}
|
|
2507
2532
|
];
|
|
@@ -2563,7 +2588,7 @@ async function writePulledSkillsToScopedMirror(pullResponse, localSkills, previo
|
|
|
2563
2588
|
console.warn(`[Notis] ${message}`, error);
|
|
2564
2589
|
};
|
|
2565
2590
|
let downloaded = 0;
|
|
2566
|
-
for (const cloudSkill of pullResponse.skills) {
|
|
2591
|
+
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
2567
2592
|
if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
2568
2593
|
continue;
|
|
2569
2594
|
}
|
|
@@ -2718,7 +2743,8 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2718
2743
|
localSkills,
|
|
2719
2744
|
previousState,
|
|
2720
2745
|
cloudCuratedSkillNames,
|
|
2721
|
-
new Set(pullResponse.skills.map((skill) => skill.name))
|
|
2746
|
+
new Set(pullResponse.skills.map((skill) => skill.name)),
|
|
2747
|
+
new Set(pullResponse.skills.filter((skill) => skill.owner_app_id).map((skill) => skill.name))
|
|
2722
2748
|
);
|
|
2723
2749
|
const failedPushes = [];
|
|
2724
2750
|
if (pushCandidates.length > 0) {
|
|
@@ -2742,13 +2768,15 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2742
2768
|
}
|
|
2743
2769
|
}
|
|
2744
2770
|
const failedDownloads = [];
|
|
2771
|
+
const writtenSkillNames = /* @__PURE__ */ new Set();
|
|
2745
2772
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
2746
2773
|
pullResponse,
|
|
2747
2774
|
localSkills,
|
|
2748
2775
|
previousState,
|
|
2749
2776
|
syncPaths,
|
|
2750
2777
|
deps,
|
|
2751
|
-
failedDownloads
|
|
2778
|
+
failedDownloads,
|
|
2779
|
+
writtenSkillNames
|
|
2752
2780
|
);
|
|
2753
2781
|
const finalLocalSkills = (await deps.scanLocalSkills(syncPaths)).filter((skill) => !BASE_SKILL_NAMES.has(skill.name));
|
|
2754
2782
|
const symlinkResult = await deps.syncSymlinks(
|
|
@@ -2759,7 +2787,14 @@ async function runSkillSync(serverUrl, jwt, dependencies = {}, options = {}) {
|
|
|
2759
2787
|
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
2760
2788
|
const lastSyncedAt = pullResponse.last_synced_at || (/* @__PURE__ */ new Date()).toISOString();
|
|
2761
2789
|
await deps.writeSyncState(
|
|
2762
|
-
buildSyncState(
|
|
2790
|
+
buildSyncState(
|
|
2791
|
+
pullResponse,
|
|
2792
|
+
finalLocalSkills,
|
|
2793
|
+
lastSyncedAt,
|
|
2794
|
+
verifiedLinks,
|
|
2795
|
+
new Set(failedDownloads.map((item) => item.name)),
|
|
2796
|
+
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames)
|
|
2797
|
+
),
|
|
2763
2798
|
syncPaths
|
|
2764
2799
|
);
|
|
2765
2800
|
return {
|
package/package.json
CHANGED
|
@@ -126,12 +126,12 @@ export function resolveBuiltBundleDir(projectDir) {
|
|
|
126
126
|
|
|
127
127
|
function normalizeShadowScopedCss(css) {
|
|
128
128
|
return css
|
|
129
|
-
//
|
|
130
|
-
// the
|
|
131
|
-
|
|
132
|
-
.replace(
|
|
133
|
-
.replace(/:root\s*\{/g, '
|
|
134
|
-
.replace(/html\s*\{/g, '
|
|
129
|
+
// Keep defaults on both the Portal shadow host and the app root used
|
|
130
|
+
// by the standalone harness. Removing :root erases Tailwind v4 theme
|
|
131
|
+
// variables in previews, collapsing spacing, type sizes and controls.
|
|
132
|
+
.replace(/(?:html|:root)\s*,\s*:host\s*\{/g, '[data-notis-app-root],:host{')
|
|
133
|
+
.replace(/:root\s*\{/g, '[data-notis-app-root],:host{')
|
|
134
|
+
.replace(/html\s*\{/g, '[data-notis-app-root],:host{')
|
|
135
135
|
// Shadow trees do not contain a body element. Route those defaults to the
|
|
136
136
|
// app root contract instead so authors still get the expected reset.
|
|
137
137
|
.replace(/body\s*\{/g, '[data-notis-app-root]{');
|
package/src/runtime/oauth.js
CHANGED
|
@@ -407,7 +407,7 @@ function connectedCallbackHtml({ portalOrigin }) {
|
|
|
407
407
|
<button class="button button-primary" id="download-desktop" type="button">Download Notis Desktop</button>
|
|
408
408
|
<a class="button" href="${escapeHtml(webAppUrl)}">Open Web App</a>
|
|
409
409
|
</div>
|
|
410
|
-
<p class="helper">The CLI stays connected even if you continue in the web app.</p>
|
|
410
|
+
<p class="helper">Desktop is available for macOS and Windows. The CLI stays connected even if you continue in the web app.</p>
|
|
411
411
|
</section>
|
|
412
412
|
<section class="next-step" id="desktop-ready-view" aria-labelledby="desktop-ready-title" hidden>
|
|
413
413
|
<div class="status"><span class="status-dot"></span> Download started</div>
|
|
@@ -433,7 +433,6 @@ function connectedCallbackHtml({ portalOrigin }) {
|
|
|
433
433
|
const resolveDownload = async () => {
|
|
434
434
|
const ua = navigator.userAgent || '';
|
|
435
435
|
if (/Windows/i.test(ua)) return downloads.base + '/win32/x64/notis-x64.exe';
|
|
436
|
-
if (/Linux/i.test(ua) && !/Android/i.test(ua)) return downloads.base + '/linux/x64/notis-linux-x64.zip';
|
|
437
436
|
if (/Macintosh|Mac OS X/i.test(ua)) {
|
|
438
437
|
let architecture = '';
|
|
439
438
|
try {
|
|
@@ -450,6 +449,7 @@ function connectedCallbackHtml({ portalOrigin }) {
|
|
|
450
449
|
const startDownload = async () => {
|
|
451
450
|
const downloadUrl = await resolveDownload();
|
|
452
451
|
window.open(downloadUrl, '_blank', 'noopener,noreferrer');
|
|
452
|
+
if (downloadUrl === downloads.fallback) return;
|
|
453
453
|
connectedView.hidden = true;
|
|
454
454
|
readyView.hidden = false;
|
|
455
455
|
document.title = 'Quick login to Notis Desktop';
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
syncSymlinks,
|
|
33
33
|
type DeletedAgentSymlink,
|
|
34
34
|
} from "./symlink-manager";
|
|
35
|
-
import { getPushCandidates } from "./sync-plan";
|
|
35
|
+
import { getPushCandidates, selectCloudSkillsToApply } from "./sync-plan";
|
|
36
36
|
import { writeCloudSkillWithBundleFallback } from "./write-cloud-skill";
|
|
37
37
|
export { fetchSyncSettings } from './cloud-client';
|
|
38
38
|
|
|
@@ -179,6 +179,24 @@ export function shouldWriteCloudSkill(
|
|
|
179
179
|
if (previous?.folderHash === localSkill.folderHash
|
|
180
180
|
&& previous.cloudContentHash === cloudContentHash(cloudSkill)) return false;
|
|
181
181
|
|
|
182
|
+
// Without a stored content baseline (skills served through a signed source URL keep
|
|
183
|
+
// none, because that URL changes on every pull) the only other signal is
|
|
184
|
+
// `skill_folder_hash`, which the server does NOT always compute the way the local
|
|
185
|
+
// folder hash is computed. An app-published skill hashes its bundle entries, so that
|
|
186
|
+
// value can never equal the hash of the extracted directory, and comparing the two
|
|
187
|
+
// replaced the folder on every sync tick, deleting whatever a running skill had
|
|
188
|
+
// written inside it. Compare cloud against cloud instead: rewrite when the cloud
|
|
189
|
+
// revision we last applied has actually moved.
|
|
190
|
+
if (previous
|
|
191
|
+
&& previous.cloudContentHash === undefined
|
|
192
|
+
&& previous.cloudId === cloudSkill.id
|
|
193
|
+
&& previous.appliedCloudFolderHash !== undefined
|
|
194
|
+
&& previous.appliedCloudFolderHash === cloudHash
|
|
195
|
+
&& previous.cloudUpdatedAt === cloudSkill.updated_at
|
|
196
|
+
&& previous.folderHash === localSkill.folderHash) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
if (cloudSkill.source === "curated") {
|
|
183
201
|
return cloudHash ? cloudHash !== localSkill.folderHash : true;
|
|
184
202
|
}
|
|
@@ -192,17 +210,42 @@ export function shouldWriteCloudSkill(
|
|
|
192
210
|
);
|
|
193
211
|
}
|
|
194
212
|
|
|
213
|
+
type AppliedCloudRevisions = Record<string, string | undefined>;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The cloud revision each folder's last successful write applied: this run's revision
|
|
217
|
+
* for the skills we just wrote, the previously recorded one otherwise. A failed write
|
|
218
|
+
* records nothing, so the next sync retries it.
|
|
219
|
+
*/
|
|
220
|
+
function collectAppliedCloudRevisions(
|
|
221
|
+
pullResponse: SyncPullResponse,
|
|
222
|
+
previousState: NotisSyncState,
|
|
223
|
+
writtenSkillNames: ReadonlySet<string>,
|
|
224
|
+
): AppliedCloudRevisions {
|
|
225
|
+
const applied: AppliedCloudRevisions = {};
|
|
226
|
+
for (const skill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
227
|
+
applied[skill.name] = writtenSkillNames.has(skill.name)
|
|
228
|
+
? skill.skill_folder_hash || ""
|
|
229
|
+
: previousState.skills[skill.name]?.appliedCloudFolderHash;
|
|
230
|
+
}
|
|
231
|
+
return applied;
|
|
232
|
+
}
|
|
233
|
+
|
|
195
234
|
function buildSyncState(
|
|
196
235
|
pullResponse: SyncPullResponse,
|
|
197
236
|
localSkills: LocalSkill[],
|
|
198
237
|
lastSyncedAt: string | null,
|
|
199
238
|
verifiedAgentLinks: Record<string, Partial<AgentTargets>> = {},
|
|
200
239
|
failedContentNames: ReadonlySet<string> = new Set(),
|
|
240
|
+
appliedRevisions: AppliedCloudRevisions = {},
|
|
201
241
|
): NotisSyncState {
|
|
202
242
|
const localSkillMap = toSkillMap(localSkills);
|
|
243
|
+
// The same row selection the writer used, so the state describes the revision that
|
|
244
|
+
// is actually on disk rather than whichever duplicate row the server listed last.
|
|
203
245
|
const skills = Object.fromEntries(
|
|
204
|
-
pullResponse.skills.map((skill) => {
|
|
246
|
+
selectCloudSkillsToApply(pullResponse.skills).map((skill) => {
|
|
205
247
|
const localSkill = localSkillMap.get(skill.name);
|
|
248
|
+
const appliedCloudFolderHash = appliedRevisions[skill.name];
|
|
206
249
|
return [
|
|
207
250
|
skill.name,
|
|
208
251
|
{
|
|
@@ -213,6 +256,7 @@ function buildSyncState(
|
|
|
213
256
|
cloudUpdatedAt: skill.updated_at,
|
|
214
257
|
...(!failedContentNames.has(skill.name) && !skill.skill_source_url
|
|
215
258
|
? { cloudContentHash: cloudContentHash(skill) } : {}),
|
|
259
|
+
...(appliedCloudFolderHash !== undefined ? { appliedCloudFolderHash } : {}),
|
|
216
260
|
syncedAt: lastSyncedAt || new Date().toISOString(),
|
|
217
261
|
},
|
|
218
262
|
];
|
|
@@ -305,7 +349,7 @@ async function writePulledSkillsToScopedMirror(
|
|
|
305
349
|
};
|
|
306
350
|
|
|
307
351
|
let downloaded = 0;
|
|
308
|
-
for (const cloudSkill of pullResponse.skills) {
|
|
352
|
+
for (const cloudSkill of selectCloudSkillsToApply(pullResponse.skills)) {
|
|
309
353
|
if (!shouldWriteCloudSkill(cloudSkill, localSkillMap, previousState)) {
|
|
310
354
|
continue;
|
|
311
355
|
}
|
|
@@ -420,6 +464,7 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
420
464
|
const materializedState = buildSyncState(
|
|
421
465
|
pullResponse, finalLocalSkills, lastSyncedAt, verifiedLinks,
|
|
422
466
|
new Set(failedDownloads.map(item => item.name)),
|
|
467
|
+
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
423
468
|
);
|
|
424
469
|
// Pull-only refresh is not an upload acknowledgement. Keep content baselines
|
|
425
470
|
// unless we actually wrote cloud content, and retain cloud-missing entries so
|
|
@@ -626,6 +671,7 @@ export async function runSkillSync(
|
|
|
626
671
|
previousState,
|
|
627
672
|
cloudCuratedSkillNames,
|
|
628
673
|
new Set(pullResponse.skills.map((skill) => skill.name)),
|
|
674
|
+
new Set(pullResponse.skills.filter((skill) => skill.owner_app_id).map((skill) => skill.name)),
|
|
629
675
|
);
|
|
630
676
|
|
|
631
677
|
const failedPushes: SkillSyncFailure[] = [];
|
|
@@ -654,6 +700,7 @@ export async function runSkillSync(
|
|
|
654
700
|
}
|
|
655
701
|
|
|
656
702
|
const failedDownloads: SkillSyncFailure[] = [];
|
|
703
|
+
const writtenSkillNames = new Set<string>();
|
|
657
704
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
658
705
|
pullResponse,
|
|
659
706
|
localSkills,
|
|
@@ -661,6 +708,7 @@ export async function runSkillSync(
|
|
|
661
708
|
syncPaths,
|
|
662
709
|
deps,
|
|
663
710
|
failedDownloads,
|
|
711
|
+
writtenSkillNames,
|
|
664
712
|
);
|
|
665
713
|
|
|
666
714
|
const finalLocalSkills = (await deps.scanLocalSkills(syncPaths))
|
|
@@ -674,7 +722,14 @@ export async function runSkillSync(
|
|
|
674
722
|
const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
|
|
675
723
|
|
|
676
724
|
await deps.writeSyncState(
|
|
677
|
-
buildSyncState(
|
|
725
|
+
buildSyncState(
|
|
726
|
+
pullResponse,
|
|
727
|
+
finalLocalSkills,
|
|
728
|
+
lastSyncedAt,
|
|
729
|
+
verifiedLinks,
|
|
730
|
+
new Set(failedDownloads.map(item => item.name)),
|
|
731
|
+
collectAppliedCloudRevisions(pullResponse, previousState, writtenSkillNames),
|
|
732
|
+
),
|
|
678
733
|
syncPaths,
|
|
679
734
|
);
|
|
680
735
|
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
import type { LocalSkill, NotisSyncState } from "./types";
|
|
1
|
+
import type { CloudSkill, LocalSkill, NotisSyncState } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One cloud row per local folder. Several active rows can carry the same skill name
|
|
5
|
+
* (an app's source skill plus its installed clone, or duplicates created by earlier
|
|
6
|
+
* pushes), and every one of them writes to the same directory. Applying them all in
|
|
7
|
+
* one pass replaced that directory several times per sync and left whichever row came
|
|
8
|
+
* last on disk. The newest revision wins, deterministically.
|
|
9
|
+
*/
|
|
10
|
+
export function selectCloudSkillsToApply(skills: CloudSkill[]): CloudSkill[] {
|
|
11
|
+
const winners = new Map<string, CloudSkill>();
|
|
12
|
+
for (const skill of skills) {
|
|
13
|
+
const current = winners.get(skill.name);
|
|
14
|
+
if (!current || (skill.updated_at || "") >= (current.updated_at || "")) {
|
|
15
|
+
winners.set(skill.name, skill);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...winners.values()];
|
|
19
|
+
}
|
|
2
20
|
|
|
3
21
|
export function getPushCandidates(
|
|
4
22
|
localSkills: LocalSkill[],
|
|
5
23
|
syncState: NotisSyncState,
|
|
6
24
|
cloudCuratedSkillNames: ReadonlySet<string> = new Set(),
|
|
7
25
|
cloudSkillNames?: ReadonlySet<string>,
|
|
26
|
+
cloudAppOwnedSkillNames: ReadonlySet<string> = new Set(),
|
|
8
27
|
): LocalSkill[] {
|
|
9
28
|
return localSkills.filter((skill) => {
|
|
10
29
|
if (cloudCuratedSkillNames.has(skill.name)) {
|
|
11
30
|
return false;
|
|
12
31
|
}
|
|
32
|
+
// App-owned content belongs to the installed app's release, not to this account.
|
|
33
|
+
// Pushing it created a second row with the same name on every local difference,
|
|
34
|
+
// and each new row then fought over the same folder.
|
|
35
|
+
if (cloudAppOwnedSkillNames.has(skill.name)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
13
38
|
const previous = syncState.skills[skill.name];
|
|
14
39
|
if (!previous) {
|
|
15
40
|
return true;
|
|
@@ -14,6 +14,11 @@ export interface SyncedSkill {
|
|
|
14
14
|
cloudUpdatedAt?: string;
|
|
15
15
|
/** Content accepted on disk; independent of server-specific folder hash formats. */
|
|
16
16
|
cloudContentHash?: string;
|
|
17
|
+
/** `skill_folder_hash` of the cloud revision a successful write actually applied.
|
|
18
|
+
* The server may compute that hash with a different construction than the local
|
|
19
|
+
* folder hash (app-published skills do), so only cloud-to-cloud comparison tells
|
|
20
|
+
* "the cloud moved" apart from "the two sides hash differently". */
|
|
21
|
+
appliedCloudFolderHash?: string;
|
|
17
22
|
syncedAt: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
@@ -49,6 +54,9 @@ export interface CloudSkill {
|
|
|
49
54
|
skill_source_url?: string | null;
|
|
50
55
|
bundle_files?: BundleFile[] | null;
|
|
51
56
|
bundle_hydration_failed?: boolean | null;
|
|
57
|
+
/** Set when the skill's content is owned by an installed Notis app. Its folder is
|
|
58
|
+
* republished by the app, never by a local push. */
|
|
59
|
+
owner_app_id?: string | null;
|
|
52
60
|
source: string;
|
|
53
61
|
status: string;
|
|
54
62
|
}
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
</head>
|
|
100
100
|
<body>
|
|
101
101
|
<div id="harness-status">harness: booting</div>
|
|
102
|
-
<div id="root"></div>
|
|
102
|
+
<div id="root" data-notis-app-root></div>
|
|
103
103
|
<script type="module">
|
|
104
104
|
const routeExport = {{ROUTE_EXPORT}};
|
|
105
105
|
const descriptor = {{RUNTIME_DESCRIPTOR}};
|