@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.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.
Files changed (216) hide show
  1. package/README.md +822 -135
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_boundary_rules.json +50 -0
  5. package/config/notis_app_design_rules.json +135 -0
  6. package/config/portal-route-roots.json +48 -0
  7. package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
  8. package/dist/base-skills/notis-apps/SKILL.md +93 -0
  9. package/dist/base-skills/notis-apps/references/architecture.md +114 -0
  10. package/dist/base-skills/notis-apps/references/context.md +81 -0
  11. package/dist/base-skills/notis-apps/references/design.md +97 -0
  12. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  13. package/dist/base-skills/notis-apps/references/reading.md +67 -0
  14. package/dist/base-skills/notis-apps/references/release.md +107 -0
  15. package/dist/base-skills/notis-apps/references/sdk.md +116 -0
  16. package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
  17. package/dist/base-skills/notis-apps/references/views.md +142 -0
  18. package/dist/base-skills/notis-cli/SKILL.md +154 -0
  19. package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
  20. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  21. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  22. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  23. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  24. package/dist/base-skills/notis-query/SKILL.md +41 -0
  25. package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
  26. package/dist/base-skills/notis-query/references/documents.md +63 -0
  27. package/dist/base-skills/notis-query/references/query.md +76 -0
  28. package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
  29. package/dist/sdk/src/agentContext.ts +36 -0
  30. package/dist/sdk/src/bundleModule.ts +79 -0
  31. package/dist/sdk/src/components/Dialog.tsx +46 -0
  32. package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
  33. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  34. package/dist/sdk/src/components/Markdown.tsx +60 -0
  35. package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
  36. package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  37. package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  38. package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  39. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  40. package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  41. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  42. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  43. package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
  44. package/dist/sdk/src/components/Skeleton.tsx +24 -0
  45. package/dist/sdk/src/config.ts +239 -0
  46. package/dist/sdk/src/documents.ts +258 -0
  47. package/dist/sdk/src/hooks/useAction.ts +28 -0
  48. package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
  49. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  50. package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
  51. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  52. package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
  53. package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  54. package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  55. package/dist/sdk/src/hooks/useDocument.ts +43 -0
  56. package/dist/sdk/src/hooks/useDocuments.ts +84 -0
  57. package/dist/sdk/src/hooks/useHandover.ts +78 -0
  58. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  59. package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
  60. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  61. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  62. package/dist/sdk/src/hooks/useNotis.ts +42 -0
  63. package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
  64. package/dist/sdk/src/hooks/useQuery.ts +74 -0
  65. package/dist/sdk/src/hooks/useTool.ts +65 -0
  66. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  67. package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
  68. package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
  69. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  70. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  71. package/dist/sdk/src/index.ts +200 -0
  72. package/dist/sdk/src/interactions/actions.ts +59 -0
  73. package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
  74. package/dist/sdk/src/interactions/visibility.ts +13 -0
  75. package/dist/sdk/src/interactions.ts +45 -0
  76. package/dist/sdk/src/nativeData.ts +39 -0
  77. package/dist/sdk/src/presentation.tsx +82 -0
  78. package/dist/sdk/src/provider.tsx +49 -0
  79. package/dist/sdk/src/queryCache.ts +172 -0
  80. package/dist/sdk/src/queryErrors.ts +51 -0
  81. package/dist/sdk/src/runtime.ts +603 -0
  82. package/dist/sdk/src/space.ts +425 -0
  83. package/dist/sdk/src/styles.css +290 -0
  84. package/dist/sdk/src/tailwind.ts +66 -0
  85. package/dist/sdk/src/vite.ts +83 -0
  86. package/dist/skill-sync/index.js +2143 -0
  87. package/dist/skill-sync/index.js.map +7 -0
  88. package/dist/skill-sync-worker.mjs +3487 -0
  89. package/dist/space-harness/host.js +66 -0
  90. package/dist/view-renderer/dist/host.css +1 -0
  91. package/dist/view-renderer/dist/host.js +1042 -0
  92. package/dist/view-renderer/dist/theme.css +73 -0
  93. package/dist/view-renderer/src/dom.js +180 -0
  94. package/dist/view-renderer/src/errors.js +8 -0
  95. package/dist/view-renderer/src/host.tsx +173 -0
  96. package/dist/view-renderer/src/index.js +4 -0
  97. package/dist/view-renderer/src/pool.js +76 -0
  98. package/dist/view-renderer/src/remote-broker.js +59 -0
  99. package/dist/view-renderer/src/renderer.js +201 -0
  100. package/package.json +25 -8
  101. package/skills/notis-apps/cli.md +406 -0
  102. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  103. package/skills/notis-onboarding/BRIEF.md +133 -0
  104. package/skills/notis-query/cli.md +39 -0
  105. package/src/agent-hook-entry.js +5 -0
  106. package/src/cli.js +294 -25
  107. package/src/command-specs/agents.js +392 -0
  108. package/src/command-specs/apps.js +1476 -203
  109. package/src/command-specs/auth.js +114 -137
  110. package/src/command-specs/diagnostics.js +730 -0
  111. package/src/command-specs/files.js +29 -0
  112. package/src/command-specs/handover.js +374 -0
  113. package/src/command-specs/helpers.js +86 -82
  114. package/src/command-specs/index.js +31 -6
  115. package/src/command-specs/meta.js +150 -18
  116. package/src/command-specs/onboarding.js +290 -0
  117. package/src/command-specs/profile.js +358 -0
  118. package/src/command-specs/skills.js +304 -0
  119. package/src/command-specs/smoke.js +386 -0
  120. package/src/command-specs/spaces.js +572 -0
  121. package/src/command-specs/tools.js +455 -139
  122. package/src/command-specs/views.js +69 -0
  123. package/src/runtime/agent-browser.js +700 -0
  124. package/src/runtime/agent-memory-state.js +126 -0
  125. package/src/runtime/agent-setup.js +383 -0
  126. package/src/runtime/app-boundary-validator.js +407 -0
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-data-literals.js +82 -0
  129. package/src/runtime/app-platform.js +2752 -237
  130. package/src/runtime/app-registry-scaffolds.js +367 -0
  131. package/src/runtime/app-test-server.js +293 -0
  132. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  133. package/src/runtime/auth-recovery.js +110 -0
  134. package/src/runtime/base-skills.d.ts +20 -0
  135. package/src/runtime/base-skills.js +167 -0
  136. package/src/runtime/channel.js +133 -0
  137. package/src/runtime/confined-space-build.js +74 -0
  138. package/src/runtime/confined-space-verify.js +49 -0
  139. package/src/runtime/delegated-context.js +68 -0
  140. package/src/runtime/errors.js +1 -0
  141. package/src/runtime/git.js +233 -0
  142. package/src/runtime/login-listener.js +15 -0
  143. package/src/runtime/native-record-files.js +47 -0
  144. package/src/runtime/oauth.js +2622 -0
  145. package/src/runtime/output.js +37 -5
  146. package/src/runtime/ports.js +31 -0
  147. package/src/runtime/profiles.js +906 -55
  148. package/src/runtime/retired-sdk-files.js +20 -0
  149. package/src/runtime/screenshot-framing.js +85 -0
  150. package/src/runtime/skill-sync/cloud-client.ts +121 -0
  151. package/src/runtime/skill-sync/index.ts +905 -0
  152. package/src/runtime/skill-sync/local-scanner.ts +1259 -0
  153. package/src/runtime/skill-sync/native-identity.ts +102 -0
  154. package/src/runtime/skill-sync/native-plan.ts +59 -0
  155. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  156. package/src/runtime/skill-sync/sync-plan.ts +54 -0
  157. package/src/runtime/skill-sync/types.ts +157 -0
  158. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  159. package/src/runtime/skill-sync-service.js +109 -0
  160. package/src/runtime/source-root.js +32 -0
  161. package/src/runtime/space-collection-scaffold.js +66 -0
  162. package/src/runtime/space-database-structures.js +229 -0
  163. package/src/runtime/space-fixture-broker.js +50 -0
  164. package/src/runtime/space-lock.js +337 -0
  165. package/src/runtime/space-platform.js +105 -0
  166. package/src/runtime/space-source.js +281 -0
  167. package/src/runtime/space-test-server.js +327 -0
  168. package/src/runtime/space-verification.js +89 -0
  169. package/src/runtime/space-verify-browser-worker.js +51 -0
  170. package/src/runtime/space-view-manifest.js +254 -0
  171. package/src/runtime/store-screenshot.js +146 -0
  172. package/src/runtime/sync-skills.d.ts +37 -0
  173. package/src/runtime/sync-skills.js +231 -0
  174. package/src/runtime/telemetry.js +92 -0
  175. package/src/runtime/transport.js +331 -45
  176. package/src/runtime/view-render.js +49 -0
  177. package/src/skill-sync-worker-entry.js +2 -0
  178. package/src/skill-sync-worker.js +50 -0
  179. package/src/space-harness-host.ts +103 -0
  180. package/template/.harness/index.html.tmpl +435 -0
  181. package/template/CHANGELOG.md +5 -0
  182. package/template/app/globals.css +29 -3
  183. package/template/app/layout.tsx +6 -3
  184. package/template/app/page.tsx +49 -42
  185. package/template/components/page-heading.tsx +23 -0
  186. package/template/components/ui/badge.tsx +7 -4
  187. package/template/components/ui/button.tsx +1 -1
  188. package/template/components/ui/card.tsx +24 -11
  189. package/template/components/ui/native-select.tsx +24 -0
  190. package/template/notis.config.ts +24 -6
  191. package/template/package-lock.json +3642 -0
  192. package/template/package.json +19 -16
  193. package/template/postcss.config.mjs +1 -1
  194. package/template/tailwind.config.ts +1 -6
  195. package/template/tsconfig.json +1 -0
  196. package/src/command-specs/db.js +0 -163
  197. package/src/runtime/app-preview-server.js +0 -312
  198. package/template/packages/notis-sdk/src/config.ts +0 -48
  199. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  200. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  201. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  202. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  203. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  204. package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
  205. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
  206. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  207. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  208. package/template/packages/notis-sdk/src/index.ts +0 -47
  209. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  210. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  211. package/template/packages/notis-sdk/src/styles.css +0 -123
  212. package/template/packages/notis-sdk/src/vite.ts +0 -54
  213. package/template/packages/notis-sdk/tsconfig.json +0 -15
  214. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
  215. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
  216. /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
@@ -0,0 +1,89 @@
1
+ /** Frozen offline fixtures rendered by the same read-only library as CLI/service. */
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { appFilesDigest } from './app-platform.js';
6
+ import { createSpaceFixtureBroker } from './space-fixture-broker.js';
7
+
8
+ // Kept for the standalone legacy worker's lifecycle tests while that diagnostic
9
+ // entrypoint is retired. The primary verifier below never starts that worker.
10
+ export function spaceVerificationEnvironment(environment, { configPath }) {
11
+ const names = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'TMP', 'LANG', 'SystemRoot', 'WINDIR', 'DISPLAY', 'XDG_RUNTIME_DIR'];
12
+ return { ...Object.fromEntries(names.filter(name => typeof environment[name] === 'string').map(name => [name, environment[name]])),
13
+ AGENT_BROWSER_CONFIG: configPath, AGENT_BROWSER_NO_WEBMCP: '1' };
14
+ }
15
+
16
+ export async function loadSpaceVerificationRenderer() {
17
+ const candidates = [new URL('../../dist/view-renderer/', import.meta.url), new URL('../../../view-renderer/', import.meta.url)];
18
+ for (const root of candidates) {
19
+ const entry = new URL('src/index.js', root);
20
+ if (!existsSync(fileURLToPath(entry))) continue;
21
+ const digest = createHash('sha256');
22
+ for (const file of ['src/renderer.js', 'src/dom.js', 'src/pool.js', 'dist/host.js', 'dist/host.css', 'dist/theme.css']) {
23
+ digest.update(file); digest.update(readFileSync(new URL(file, root)));
24
+ }
25
+ return { ...(await import(entry.href)), rendererDigest: digest.digest('hex') };
26
+ }
27
+ throw new Error('The shared view renderer is missing. Rebuild or reinstall this CLI package.');
28
+ }
29
+
30
+ // One browser per verification, two widths concurrently; no worker creates an
31
+ // unbounded browser fanout. Up to two invocations may run simultaneously.
32
+ let active = 0;
33
+ const waiting = [];
34
+ async function acquire(signal) {
35
+ if (signal?.aborted) throw new Error('Verification cancelled.');
36
+ if (active < 2) { active++; return; }
37
+ if (waiting.length >= 8) throw new Error('The local verification queue is full.');
38
+ await new Promise((accept, reject) => {
39
+ const entry = { accept: () => { signal?.removeEventListener('abort', abort); accept(); } };
40
+ const abort = () => { const index = waiting.indexOf(entry); if(index >= 0) waiting.splice(index, 1); reject(new Error('Verification cancelled.')); };
41
+ waiting.push(entry); signal?.addEventListener('abort', abort, { once: true });
42
+ });
43
+ }
44
+ function releaseSlot() { const next = waiting.shift(); if (next) next.accept(); else active--; }
45
+
46
+ /** Initial deployment checks the exact captured bytes at both product widths. */
47
+ export async function verifySpaceRelease(release, { capabilities, timeoutMs = 45_000, signal, loadRenderer = loadSpaceVerificationRenderer,
48
+ authorizationMode = 'authorize_missing', unavailableActions = [] } = {}) {
49
+ const diagnostic = { schema: 'notis-space-render/v1', local_key: release.manifest.local_key,
50
+ source_digest: appFilesDigest(release.sourceFiles), artifact_digest: appFilesDigest(release.files),
51
+ mode: 'offline_fixtures', checked_at: new Date().toISOString(),
52
+ ...(authorizationMode === 'reuse_only' ? { authorization_mode: authorizationMode,
53
+ unavailable_actions: structuredClone(unavailableActions) } : {}) };
54
+ if (signal?.aborted) return { ...diagnostic, status: 'failed', errors: [{ phase: 'browser', message: 'Verification cancelled.' }] };
55
+ if (release.manifest.kind === 'container') return { ...diagnostic, status: 'not_applicable_container' };
56
+ let pool, held = false, rendererDigest;
57
+ const brokers = [390, 1440].map(width => ({ width, broker: null }));
58
+ try {
59
+ // Validate fixture authority before opening a browser or acquiring a slot.
60
+ for (const item of brokers) item.broker = createSpaceFixtureBroker({ release, capabilities, width: item.width,
61
+ authorizationMode, unavailableActions });
62
+ await acquire(signal); held = true;
63
+ const library = await loadRenderer(); rendererDigest = library.rendererDigest;
64
+ pool = library.createBrowserPool({ concurrency: 2, maxQueue: 2 });
65
+ const observations = await Promise.allSettled(brokers.map(async ({ width, broker }) => {
66
+ const result = await library.renderView({ broker, pool, outputs: ['markdown', 'screenshot'], width, timeoutMs, signal });
67
+ const blocked = result.blocked_calls || [];
68
+ return { width, mounted: true, status: blocked.length ? 'failed' : 'passed',
69
+ markdown_sha256: createHash('sha256').update(result.markdown).digest('hex'),
70
+ screenshot_sha256: createHash('sha256').update(Buffer.from(result.screenshot.data_base64, 'base64')).digest('hex'),
71
+ screenshot_width: result.screenshot.width, screenshot_height: result.screenshot.height,
72
+ rendered_at: result.rendered_at, blocked_calls: blocked };
73
+ }));
74
+ const renders = observations.map((value, index) => value.status === 'fulfilled' ? value.value : {
75
+ width: brokers[index].width, mounted: false, status: 'failed', error: value.reason?.code || 'render_failed',
76
+ });
77
+ const runtimeCalls = brokers.flatMap(({ width, broker }) => broker.calls.map(call => ({ width, ...call })));
78
+ const passed = renders.every(value => value.status === 'passed') && runtimeCalls.every(call => call.ok);
79
+ return { ...diagnostic, status: passed ? 'passed' : 'failed', mounted: renders.every(value => value.mounted),
80
+ fixture_digest: brokers[0].broker.fixtureDigest, renderer_digest: rendererDigest,
81
+ render_count: renders.length, browser_count: 1, renders, runtimeCalls,
82
+ errors: passed ? [] : [{ phase: 'renderer', message: 'At least one width or read-only fixture failed verification.' }] };
83
+ } catch (error) {
84
+ return { ...diagnostic, status: 'failed', ...(rendererDigest ? { renderer_digest: rendererDigest } : {}),
85
+ errors: [{ phase: 'harness', message: error.message }] };
86
+ } finally {
87
+ try { await pool?.close(); } finally { if (held) releaseSlot(); }
88
+ }
89
+ }
@@ -0,0 +1,51 @@
1
+ /** Runs in a credential-free child process, never in the authenticated CLI. */
2
+ import { closeAgentBrowserSessionResult, isAgentBrowserAvailable, runHarnessRoute } from './agent-browser.js';
3
+
4
+ let sessionName;
5
+ let closing;
6
+ let started = false;
7
+ const controller = new AbortController();
8
+ let exitCode = 0;
9
+ const cleanup = () => closing ||= sessionName ? closeAgentBrowserSessionResult(sessionName) : Promise.resolve({ ok: true });
10
+ for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => {
11
+ exitCode = signal === 'SIGINT' ? 130 : 143;
12
+ controller.abort();
13
+ if (!started) process.exit(exitCode);
14
+ });
15
+ process.on('disconnect', () => { exitCode = 1; controller.abort(); if (!started) process.exit(1); });
16
+
17
+ process.once('message', async input => {
18
+ started = true;
19
+ let result;
20
+ try {
21
+ const url = new URL(input.url);
22
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port
23
+ || !/^notis-space-[a-f0-9-]+$/.test(input.sessionName)) throw new Error('Invalid offline verification session.');
24
+ sessionName = input.sessionName;
25
+ if (!isAgentBrowserAvailable()) throw new Error('Install agent-browser and its Chromium browser to verify a presentation.');
26
+ const observed = await runHarnessRoute({ url: url.href, sessionName, timeoutMs: input.timeoutMs,
27
+ designViewports: [], waitForRuntime: true, signal: controller.signal });
28
+ // Never retain raw eval/stdout or action inputs/results in a diagnostic receipt.
29
+ result = {
30
+ mounted: observed.mounted,
31
+ renderStarted: observed.renderStarted,
32
+ timed_out: Boolean(observed.timed_out),
33
+ errors: (observed.errors || []).map(error => ({ phase: String(error.phase).slice(0, 100), message: String(error.message).slice(0, 2000) })),
34
+ runtimeCalls: (observed.runtimeCalls || []).map(call => ({ actionId: call.actionId, ok: call.ok,
35
+ ...(call.error ? { error: String(call.error).slice(0, 2000) } : {}) })),
36
+ ...(observed.tool_error ? { tool_error: { phase: observed.tool_error.phase,
37
+ message: String(observed.tool_error.stderr || observed.tool_error.message
38
+ || 'The isolated verification browser could not complete this operation.').slice(0, 2000) } } : {}),
39
+ };
40
+ } catch (error) {
41
+ result = { mounted: false, errors: [{ phase: 'browser', message: error.message }], runtimeCalls: [] };
42
+ } finally {
43
+ if (process.connected) {
44
+ try { process.send({ phase: 'cleanup' }, () => {}); } catch { /* Cleanup still owns the browser after IPC loss. */ }
45
+ }
46
+ const closed = await cleanup();
47
+ result = { ...result, cleanup: closed, ...(!closed.ok ? { cleanup_failed: true } : {}) };
48
+ }
49
+ if (process.connected) process.send(result, () => process.exit(exitCode));
50
+ else process.exit(exitCode);
51
+ });
@@ -0,0 +1,254 @@
1
+ /**
2
+ * What a view declares (V4): path, typed params, shows, memory, chrome and a Markdown override.
3
+ * Mirrors server/lib/space_view_manifest.py, which re-checks every release; property-level checks
4
+ * need the linked schemas and run on the server during `spaces verify` and deploy.
5
+ * Every presentation requires path, description and memory, including sources that omit
6
+ * specVersion. Containers remain non-executable; explicit specVersion 2 is unchanged.
7
+ */
8
+ import { usageError } from './errors.js';
9
+ import portalRouteRoots from '../../config/portal-route-roots.json' with { type: 'json' };
10
+ const RESERVED_PATHS = new Set(portalRouteRoots);
11
+
12
+ export const VIEW_SPEC_VERSION = 2;
13
+ export const VIEW_DEFINITION_FIELDS = Object.freeze(['specVersion', 'path', 'params', 'shows', 'memory', 'chrome', 'markdown']);
14
+ export const VIEW_PARAM_TYPES = Object.freeze(['record', 'enum', 'text', 'number', 'date', 'boolean']);
15
+ const RESERVED_PARAMS = new Set(['record', 'assistantThread']);
16
+ const PAGE_ONLY = ['params', 'shows', 'memory', 'chrome', 'markdown'];
17
+ const NAME = /^[a-z][A-Za-z0-9_]{0,63}$/;
18
+ // Credentials never ride in links, so no view param may look like one (the Portal drops these names).
19
+ const CREDENTIAL_PARAM = /^(?:jwt|code|state|key)$|token|secret|password|credential|session|auth|api_?key/i;
20
+ const WORD = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
+ const ID_TAIL = /(?:^|-)(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
22
+ const PLACEHOLDER = /^\{([a-z][A-Za-z0-9_]{0,63})(?:\.([^{}.\s][^{}]{0,199}))?\}$/;
23
+ const RECORD_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
25
+ const SERVICE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
26
+ // eslint-disable-next-line no-control-regex
27
+ const CONTROL = /[\u0000-\u001f\u007f]/;
28
+ const FIELD_KEYS = ['property', 'property_id', 'column'];
29
+ const OPS = new Set(['equals', 'not_equals', 'in', 'contains', 'not_contains', 'starts_with', 'ends_with', 'is_empty', 'is_not_empty',
30
+ 'greater_than', 'less_than', 'greater_than_or_equal_to', 'less_than_or_equal_to', 'does_not_contain', 'before', 'after',
31
+ 'on_or_before', 'on_or_after']);
32
+ const CRON_BOUNDS = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]];
33
+ const DEBOUNCE = [60, 86400];
34
+
35
+ const plain = value => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
36
+ const text = (value, limit) => typeof value === 'string' && value.trim().length >= 1 && value.trim().length <= limit && !CONTROL.test(value);
37
+ const isoDate = value => typeof value === 'string' && DATE.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`))
38
+ && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
39
+
40
+ export function validateViewPath(value) {
41
+ if (typeof value !== 'string' || value.length < 1 || value.length > 100) {
42
+ throw usageError('Declare path as the words of this view\'s links, such as "notes" or "seo/keywords".');
43
+ }
44
+ const segments = value.split('/');
45
+ if (segments.length > 4 || segments.some(segment => !WORD.test(segment) || ID_TAIL.test(segment))) {
46
+ throw usageError('A path is up to four lowercase words (a-z, 0-9, dashes) separated by "/", without ids.');
47
+ }
48
+ if (RESERVED_PATHS.has(segments[0])) throw usageError(`The path "${segments[0]}" belongs to the Portal. Choose another first word.`);
49
+ return value;
50
+ }
51
+
52
+ function validCron(value) {
53
+ const fields = typeof value === 'string' ? value.split(' ') : [];
54
+ if (fields.length !== 5) return false;
55
+ return fields.every((field, index) => {
56
+ const [low, high] = CRON_BOUNDS[index];
57
+ const inRange = number => /^\d+$/.test(number) && Number(number) >= low && Number(number) <= high;
58
+ if (index === 0) return inRange(field);
59
+ return field.split(',').every(part => {
60
+ const [base, step, extra] = part.split('/');
61
+ if (extra !== undefined || (step !== undefined && !(/^\d+$/.test(step) && Number(step) >= 1 && Number(step) <= high))) return false;
62
+ if (base === '*') return true;
63
+ const [start, end, more] = base.split('-');
64
+ if (more !== undefined || !inRange(start) || (end !== undefined && (!inRange(end) || Number(start) > Number(end)))) return false;
65
+ return true;
66
+ });
67
+ });
68
+ }
69
+
70
+ function validateMemory(memory) {
71
+ if (!plain(memory) || Object.keys(memory).sort().join(',') !== 'attachments,markdown,screenshot,snapshots') {
72
+ throw usageError('Declare memory as { markdown, attachments, screenshot, snapshots }; see the memory guide.');
73
+ }
74
+ if (['markdown', 'attachments', 'screenshot'].some(name => typeof memory[name] !== 'boolean')) {
75
+ throw usageError('memory.markdown, memory.attachments and memory.screenshot are true or false.');
76
+ }
77
+ if (!Array.isArray(memory.snapshots) || memory.snapshots.length > 3) {
78
+ throw usageError('memory.snapshots lists on_render, { scheduled } and { on_change }, each at most once ([] for none).');
79
+ }
80
+ const seen = new Set();
81
+ for (const strategy of memory.snapshots) {
82
+ let kind;
83
+ if (strategy === 'on_render') kind = 'on_render';
84
+ else if (plain(strategy) && Object.keys(strategy).join(',') === 'scheduled') {
85
+ kind = 'scheduled';
86
+ if (!validCron(strategy.scheduled)) throw usageError('A scheduled snapshot uses a five-field UTC cron with a fixed minute, such as "0 7 * * 1".');
87
+ } else if (plain(strategy) && Object.keys(strategy).join(',') === 'on_change') {
88
+ kind = 'on_change';
89
+ const debounce = strategy.on_change?.debounceSeconds;
90
+ if (!plain(strategy.on_change) || Object.keys(strategy.on_change).join(',') !== 'debounceSeconds' || !Number.isInteger(debounce)
91
+ || debounce < DEBOUNCE[0] || debounce > DEBOUNCE[1]) {
92
+ throw usageError(`An on_change snapshot waits debounceSeconds (${DEBOUNCE[0]} to ${DEBOUNCE[1]}) after the last change.`);
93
+ }
94
+ } else throw usageError('memory.snapshots lists on_render, { scheduled: "<cron>" } or { on_change: { debounceSeconds } }.');
95
+ if (seen.has(kind)) throw usageError('Declare each memory snapshot strategy at most once.');
96
+ seen.add(kind);
97
+ }
98
+ }
99
+
100
+ /** One typed param value, as the SDK and the server accept it. */
101
+ export function viewParamValue(declaration, value) {
102
+ switch (declaration.type) {
103
+ case 'text': return typeof value === 'string' && value.length <= 500 && !CONTROL.test(value);
104
+ case 'enum': return typeof value === 'string' && (declaration.values || []).includes(value);
105
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
106
+ case 'boolean': return typeof value === 'boolean';
107
+ case 'date': return isoDate(value);
108
+ case 'record': return typeof value === 'string' && RECORD_KEY.test(value);
109
+ default: return false;
110
+ }
111
+ }
112
+
113
+ function validateParam(name, declaration, resources) {
114
+ if (!NAME.test(name) || RESERVED_PARAMS.has(name) || CREDENTIAL_PARAM.test(name)) throw usageError(`Param names are camelCase words; "${name}" is not available.`);
115
+ const allowed = new Set(['type', 'description', 'required', 'default', 'values', 'database', 'main', 'slugProperty']);
116
+ if (!plain(declaration) || Object.keys(declaration).some(field => !allowed.has(field)) || !VIEW_PARAM_TYPES.includes(declaration.type)) {
117
+ throw usageError(`Declare param "${name}" as { type, description, ... } with type ${VIEW_PARAM_TYPES.join(', ')}.`);
118
+ }
119
+ if (!text(declaration.description, 500)) throw usageError(`Describe param "${name}": what it selects, for agents and people.`);
120
+ if ('required' in declaration && typeof declaration.required !== 'boolean') throw usageError(`Param "${name}": required is true or false.`);
121
+ if (declaration.type === 'enum') {
122
+ const values = declaration.values;
123
+ if (!Array.isArray(values) || values.length < 1 || values.length > 100 || new Set(values).size !== values.length
124
+ || values.some(value => !text(value, 100))) throw usageError(`Param "${name}": an enum lists its distinct values.`);
125
+ } else if ('values' in declaration) throw usageError(`Param "${name}": only enum params list values.`);
126
+ if (declaration.type === 'record') {
127
+ if (resources[declaration.database]?.kind !== 'database') {
128
+ throw usageError(`Param "${name}": a record param names a database this Space declares in resources.`);
129
+ }
130
+ if ('main' in declaration && typeof declaration.main !== 'boolean') throw usageError(`Param "${name}": main is true or false.`);
131
+ if ('slugProperty' in declaration && !text(declaration.slugProperty, 200)) throw usageError(`Param "${name}": slugProperty names a property of its database.`);
132
+ if ('default' in declaration) throw usageError(`Param "${name}": a record param has no default record.`);
133
+ } else if (['database', 'main', 'slugProperty'].some(field => field in declaration)) {
134
+ throw usageError(`Param "${name}": database, main and slugProperty belong to record params.`);
135
+ }
136
+ if ('default' in declaration) {
137
+ if (declaration.required === true) throw usageError(`Param "${name}": a required param has no default.`);
138
+ if (!viewParamValue(declaration, declaration.default)) throw usageError(`Param "${name}": its default must be a valid ${declaration.type} value.`);
139
+ }
140
+ }
141
+
142
+ function checkPlaceholder(value, params, collection, where) {
143
+ const match = typeof value === 'string' ? PLACEHOLDER.exec(value) : null;
144
+ if (!match) return;
145
+ const [, name, property] = match;
146
+ if (name === 'record') {
147
+ const records = Object.values(params).filter(declaration => declaration.type === 'record');
148
+ if (records.length === 1 || (!records.length && collection)) return;
149
+ throw usageError('Use {record} only in a view with exactly one record param (or a collection); otherwise name the param, such as {project}.');
150
+ }
151
+ if (!Object.hasOwn(params, name)) throw usageError(`${where}: "{${name}}" names no declared param.`);
152
+ if (property !== undefined && params[name].type !== 'record') throw usageError(`${where}: only record params have properties, such as {${name}}.`);
153
+ }
154
+
155
+ function walkFilter(node, visit, where, depth = 0, budget = { left: 200 }) {
156
+ budget.left -= 1;
157
+ if (depth > 12 || budget.left < 0) throw usageError(`${where}: the filter is too complex; use fewer conditions.`);
158
+ if (depth === 0 && plain(node) && !Object.keys(node).length) return;
159
+ if (!plain(node)) throw usageError(`${where}: a filter is an object.`);
160
+ for (const group of ['and', 'or']) {
161
+ if (!Object.hasOwn(node, group)) continue;
162
+ const children = node[group];
163
+ if (Object.keys(node).length !== 1 || !Array.isArray(children) || !children.length) throw usageError(`${where}: filter groups need at least one condition.`);
164
+ children.forEach(child => walkFilter(child, visit, where, depth + 1, budget));
165
+ return;
166
+ }
167
+ const field = node.field;
168
+ if (Object.keys(node).some(key => !['field', 'op', 'value'].includes(key)) || !('field' in node) || !('op' in node)
169
+ || !plain(field) || Object.keys(field).length !== 1 || !FIELD_KEYS.includes(Object.keys(field)[0])
170
+ || typeof Object.values(field)[0] !== 'string' || !OPS.has(node.op)) {
171
+ throw usageError(`${where}: each condition is { field: { property }, op, value } in the native filter language.`);
172
+ }
173
+ visit(node);
174
+ }
175
+
176
+ function validateShows(name, declaration, params, resources, collection) {
177
+ const where = `shows.${name}`;
178
+ if (!NAME.test(name)) throw usageError(`Shows names are camelCase words; "${name}" is not available.`);
179
+ if (plain(declaration) && 'about' in declaration) {
180
+ const services = declaration.services ?? [];
181
+ if (Object.keys(declaration).some(field => !['about', 'services'].includes(field)) || !text(declaration.about, 500)
182
+ || !Array.isArray(services) || services.length > 20 || services.some(service => typeof service !== 'string' || !SERVICE.test(service))) {
183
+ throw usageError(`${where}: describe what it shows as { about, services? }.`);
184
+ }
185
+ return;
186
+ }
187
+ if (!plain(declaration) || Object.keys(declaration).some(field => !['database', 'where', 'open'].includes(field)) || !('where' in declaration)) {
188
+ throw usageError(`${where}: declare { database, where, open? } for Notis data or { about, services? } otherwise.`);
189
+ }
190
+ if (resources[declaration.database]?.kind !== 'database') {
191
+ throw usageError(`${where}: "${declaration.database}" is not a database this Space declares and links.`);
192
+ }
193
+ if (declaration.open !== undefined && (params[declaration.open]?.type !== 'record' || params[declaration.open].database !== declaration.database)) {
194
+ throw usageError(`${where}: open names a record param over the same database.`);
195
+ }
196
+ walkFilter(declaration.where, condition => {
197
+ if (!('value' in condition)) return;
198
+ const values = condition.op === 'in' && Array.isArray(condition.value) ? condition.value : [condition.value];
199
+ values.forEach(value => checkPlaceholder(value, params, collection, where));
200
+ }, where);
201
+ }
202
+
203
+ /**
204
+ * Validates V4 declarations independently of the optional version marker, then returns
205
+ * their manifest form without inventing a version or page fields for containers.
206
+ */
207
+ export function viewManifestFields(definition, { entry, resources }) {
208
+ const container = !entry;
209
+ const version = definition.specVersion;
210
+ if (version !== undefined && version !== VIEW_SPEC_VERSION) throw usageError(`specVersion is ${VIEW_SPEC_VERSION} for sources that declare the view manifest.`);
211
+ if (container && PAGE_ONLY.some(field => definition[field] !== undefined)) {
212
+ throw usageError('A container has no page: params, shows, memory, chrome and markdown belong to its views.');
213
+ }
214
+ if (!container || version === VIEW_SPEC_VERSION) {
215
+ const missing = ['path', 'description'].filter(field => !definition[field]);
216
+ if (!container && definition.memory === undefined) missing.push('memory');
217
+ if (missing.length) throw usageError(`This view needs ${missing.join(', ')} (specVersion 2).`);
218
+ if (!text(definition.description, 300)) throw usageError('Describe this view in one line (up to 300 characters).');
219
+ }
220
+ if (definition.path !== undefined) validateViewPath(definition.path);
221
+ const params = definition.params ?? {};
222
+ if (definition.params !== undefined) {
223
+ if (!plain(params) || Object.keys(params).length > 50) throw usageError('Declare params as { name: { type, description, ... } }.');
224
+ const mains = new Map();
225
+ for (const [name, declaration] of Object.entries(params)) {
226
+ validateParam(name, declaration, resources);
227
+ if (declaration.type !== 'record' || declaration.main !== true) continue;
228
+ if (mains.has(declaration.database)) {
229
+ throw usageError(`Params "${mains.get(declaration.database)}" and "${name}" both claim main for database "${declaration.database}"; a database has one main view.`);
230
+ }
231
+ mains.set(declaration.database, name);
232
+ }
233
+ }
234
+ if (definition.shows !== undefined) {
235
+ if (!plain(definition.shows) || Object.keys(definition.shows).length > 50) {
236
+ throw usageError('Declare shows as { name: { database, where, open? } } or { name: { about, services? } }.');
237
+ }
238
+ for (const [name, declaration] of Object.entries(definition.shows)) validateShows(name, declaration, params, resources, definition.collection);
239
+ }
240
+ if (definition.memory !== undefined) validateMemory(definition.memory);
241
+ if (definition.chrome !== undefined && !['portal', 'hidden'].includes(definition.chrome)) throw usageError("chrome is 'portal' (default) or 'hidden'.");
242
+ if (definition.markdown !== undefined && (typeof definition.markdown !== 'string' || !definition.markdown)) {
243
+ throw usageError('markdown names a source module whose default export renders this view as Markdown.');
244
+ }
245
+ return {
246
+ ...(version !== undefined ? { spec_version: version } : {}),
247
+ ...(definition.path !== undefined ? { path: definition.path } : {}),
248
+ ...(definition.params !== undefined ? { params } : {}),
249
+ ...(definition.shows !== undefined ? { shows: definition.shows } : {}),
250
+ ...(definition.memory !== undefined ? { memory: definition.memory } : {}),
251
+ ...(definition.chrome !== undefined ? { chrome: definition.chrome } : {}),
252
+ ...(definition.markdown !== undefined ? { markdown: { export_name: 'SpaceMarkdown' } } : {}),
253
+ };
254
+ }
@@ -0,0 +1,146 @@
1
+ import { fileURLToPath } from 'node:url';
2
+
3
+ const STORE_SCREENSHOT_ASPECT = 16 / 10;
4
+ const SHARED_BACKDROP_PATH = fileURLToPath(
5
+ new URL('./assets/store-screenshot-dark.png', import.meta.url),
6
+ );
7
+
8
+ const ACCENT_PALETTES = {
9
+ blue: { glow: '#4f7cff', glow2: '#274690' },
10
+ violet: { glow: '#9b87f5', glow2: '#5038a1' },
11
+ emerald: { glow: '#34d399', glow2: '#176b55' },
12
+ amber: { glow: '#f2a94a', glow2: '#8b4f20' },
13
+ rose: { glow: '#fb7185', glow2: '#8f3048' },
14
+ sky: { glow: '#38bdf8', glow2: '#176887' },
15
+ fuchsia: { glow: '#d946ef', glow2: '#772786' },
16
+ teal: { glow: '#2dd4bf', glow2: '#176e68' },
17
+ };
18
+
19
+ const ACCENT_NAMES = Object.keys(ACCENT_PALETTES);
20
+
21
+ async function loadSharp() {
22
+ return (await import('sharp')).default;
23
+ }
24
+
25
+ function stableHash(value) {
26
+ let hash = 2166136261;
27
+ for (const character of String(value || 'notis-app')) {
28
+ hash ^= character.codePointAt(0);
29
+ hash = Math.imul(hash, 16777619);
30
+ }
31
+ return hash >>> 0;
32
+ }
33
+
34
+ export function resolveStoreScreenshotAccent(accent, seed = 'notis-app') {
35
+ if (accent && ACCENT_PALETTES[accent]) {
36
+ return accent;
37
+ }
38
+ return ACCENT_NAMES[stableHash(seed) % ACCENT_NAMES.length];
39
+ }
40
+
41
+ export function storeScreenshotLayout(width = 2000, height = 1250, contentAspect = STORE_SCREENSHOT_ASPECT) {
42
+ const aspect = Number.isFinite(contentAspect) && contentAspect > 0
43
+ ? contentAspect : STORE_SCREENSHOT_ASPECT;
44
+ const horizontalInset = Math.max(48, Math.round(width * 0.06));
45
+ const verticalInset = Math.max(40, Math.round(height * 0.06));
46
+ let cardWidth = width - horizontalInset * 2;
47
+ let cardHeight = Math.round(cardWidth / aspect);
48
+ const maximumCardHeight = height - verticalInset * 2;
49
+ if (cardHeight > maximumCardHeight) {
50
+ cardHeight = maximumCardHeight;
51
+ cardWidth = Math.round(cardHeight * aspect);
52
+ }
53
+ const cardLeft = Math.round((width - cardWidth) / 2);
54
+ const cardTop = Math.round((height - cardHeight) / 2) - Math.round(height * 0.008);
55
+ return {
56
+ width,
57
+ height,
58
+ cardWidth,
59
+ cardHeight,
60
+ cardLeft,
61
+ cardTop,
62
+ radius: Math.max(18, Math.round(width * 0.016)),
63
+ };
64
+ }
65
+
66
+ async function backdropImage(layout) {
67
+ const sharp = await loadSharp();
68
+ return sharp(SHARED_BACKDROP_PATH)
69
+ .resize(layout.width, layout.height, { fit: 'cover', position: 'centre' })
70
+ .png()
71
+ .toBuffer();
72
+ }
73
+
74
+ function roundedRectSvg(width, height, radius, { fill = '#ffffff', stroke = null } = {}) {
75
+ const strokeAttribute = stroke
76
+ ? ` fill="none" stroke="${stroke}" stroke-width="2"`
77
+ : ` fill="${fill}"`;
78
+ return Buffer.from(
79
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="${radius}" ry="${radius}"${strokeAttribute}/></svg>`,
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Composite a truthful browser capture into the deterministic Store frame.
85
+ * Fit the complete source pixels; never crop controls or redraw the app UI.
86
+ */
87
+ export async function composeStoreScreenshot({
88
+ inputPath,
89
+ outputPath,
90
+ width = 2000,
91
+ height = 1250,
92
+ accent = null,
93
+ seed = 'notis-app',
94
+ focused = false,
95
+ theme = 'light',
96
+ }) {
97
+ const sharp = await loadSharp();
98
+ const resolvedTheme = theme === 'dark' ? 'dark' : 'light';
99
+ const source = await sharp(inputPath).metadata();
100
+ const layout = storeScreenshotLayout(width, height, source.width / source.height);
101
+ const resolvedAccent = resolveStoreScreenshotAccent(accent, seed);
102
+
103
+ const roundedCapture = await sharp(inputPath)
104
+ .resize(layout.cardWidth, layout.cardHeight, {
105
+ fit: 'contain',
106
+ background: resolvedTheme === 'dark' ? '#09090b' : '#ffffff',
107
+ })
108
+ .composite([{
109
+ input: roundedRectSvg(layout.cardWidth, layout.cardHeight, layout.radius),
110
+ blend: 'dest-in',
111
+ }])
112
+ .png()
113
+ .toBuffer();
114
+
115
+ const border = roundedRectSvg(
116
+ layout.cardWidth,
117
+ layout.cardHeight,
118
+ layout.radius,
119
+ { stroke: resolvedTheme === 'dark' ? '#FFFFFF2A' : '#0F172A22' },
120
+ );
121
+
122
+ await sharp(await backdropImage(layout))
123
+ .composite([
124
+ { input: roundedCapture, left: layout.cardLeft, top: layout.cardTop },
125
+ { input: border, left: layout.cardLeft, top: layout.cardTop },
126
+ ])
127
+ .png({
128
+ compressionLevel: 9,
129
+ adaptiveFiltering: true,
130
+ })
131
+ .toFile(outputPath);
132
+
133
+ return {
134
+ mode: 'framed',
135
+ accent: resolvedAccent,
136
+ focused: Boolean(focused),
137
+ theme: resolvedTheme,
138
+ card: {
139
+ left: layout.cardLeft,
140
+ top: layout.cardTop,
141
+ width: layout.cardWidth,
142
+ height: layout.cardHeight,
143
+ radius: layout.radius,
144
+ },
145
+ };
146
+ }
@@ -0,0 +1,37 @@
1
+ import type { BaseSkillReconcileResult } from './base-skills.js';
2
+
3
+ export interface ReconcileAllSkillsResult {
4
+ baseSkills: string[];
5
+ baseInstalled: number;
6
+ baseLinked: number;
7
+ baseBackups: string[];
8
+ }
9
+
10
+ export interface SkillSyncLockOptions {
11
+ home?: string;
12
+ timeoutMs?: number;
13
+ staleMs?: number;
14
+ pollMs?: number;
15
+ now?: () => number;
16
+ }
17
+
18
+ export function withSkillSyncLock<T>(
19
+ callback: () => Promise<T> | T,
20
+ options?: SkillSyncLockOptions,
21
+ ): Promise<T>;
22
+
23
+ export function reconcileAllSkills<T extends object>(options: {
24
+ serverUrl: string;
25
+ jwt: string;
26
+ honorSyncEnabled: boolean;
27
+ userId?: string | null;
28
+ home?: string;
29
+ runAccountSync: (
30
+ serverUrl: string,
31
+ jwt: string,
32
+ dependencies: Record<string, never>,
33
+ options: { honorSyncEnabled: boolean },
34
+ ) => Promise<T>;
35
+ reconcileBase?: (options: { userId?: string | null; home?: string }) => BaseSkillReconcileResult;
36
+ lockOptions?: Omit<SkillSyncLockOptions, 'home'>;
37
+ }): Promise<T & ReconcileAllSkillsResult>;