@mangtre/core 0.1.0
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/LICENSE +26 -0
- package/dist/index.d.ts +706 -0
- package/dist/index.js +102 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Măng — Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 RumitX. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its source code (the "Software"), including the Măng shell,
|
|
6
|
+
the @mangtre/* packages, and the bundled mini-apps in this repository, are the
|
|
7
|
+
confidential and proprietary property of RumitX.
|
|
8
|
+
|
|
9
|
+
No license, right, or permission is granted to any person to use, copy, modify,
|
|
10
|
+
merge, publish, distribute, sublicense, sell, or create derivative works of the
|
|
11
|
+
Software, in whole or in part, without the prior written consent of RumitX.
|
|
12
|
+
|
|
13
|
+
Unauthorized copying, distribution, or use of the Software, via any medium, is
|
|
14
|
+
strictly prohibited.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
18
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL RUMITX BE LIABLE
|
|
19
|
+
FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
Note: this proprietary notice protects the codebase during the early (NOW)
|
|
23
|
+
horizon. When Măng opens to third-party creators, a separate license may be
|
|
24
|
+
issued for the fork-able starter kit.
|
|
25
|
+
|
|
26
|
+
For licensing inquiries: RumitX — https://rumitx.com
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mangtre/core — the stable contract between the Măng shell and every mini-app.
|
|
3
|
+
*
|
|
4
|
+
* Golden rule: a mini-app NEVER touches real storage, `fetch`, or native APIs
|
|
5
|
+
* directly — always through `sdk.*`. Today these are backed by the monolith
|
|
6
|
+
* (dynamic import); later by a sandbox bridge. The app changes zero lines.
|
|
7
|
+
*
|
|
8
|
+
* See `sot/Mang_Tech_Foundations_v0.1.1.md` §2.
|
|
9
|
+
*/
|
|
10
|
+
/** Capabilities a mini-app may request. Declared up front, even if granted freely now. */
|
|
11
|
+
type Permission = "storage" | "theme" | "ai" | "identity" | "context" | "data";
|
|
12
|
+
/**
|
|
13
|
+
* Languages Măng ships. The shell owns the active locale (detect + switch + persist) and
|
|
14
|
+
* hands it to every mini-app at mount via `MangCore.locale`. Vietnamese-first; English second.
|
|
15
|
+
*/
|
|
16
|
+
type Locale = "vi" | "en";
|
|
17
|
+
/** A short string in every language Măng ships. */
|
|
18
|
+
type LocalizedText = Record<Locale, string>;
|
|
19
|
+
/**
|
|
20
|
+
* Where a mini-app feature can run. The shell can host an app on different *surfaces*, each with a
|
|
21
|
+
* different capability ceiling: `desktop` (Tauri Win/Mac — native sidecar, filesystem, heavy/local
|
|
22
|
+
* AI) is highest; `web` (browser + PWA — sandboxed, no heavy local AI) is mid; `zalo` (Zalo Mini
|
|
23
|
+
* App runtime) is most constrained. `"web"` covers PWA too (install/offline add nothing to the
|
|
24
|
+
* capability ceiling). Mobile-native is LATER — deliberately not in this union yet.
|
|
25
|
+
*
|
|
26
|
+
* Declared per feature so a single app can mix surfaces (e.g. "generate quiz with AI" → desktop
|
|
27
|
+
* only; "view/answer quiz" → everywhere). See `sot/Mang_Tech_Foundations` §2.1.
|
|
28
|
+
*/
|
|
29
|
+
type Surface = "desktop" | "web" | "zalo";
|
|
30
|
+
/**
|
|
31
|
+
* A feature a mini-app exposes to the shell so it can be searched and launched directly from the
|
|
32
|
+
* host (e.g. the command palette). Declared STATICALLY on the manifest, so the shell can index
|
|
33
|
+
* every app's features WITHOUT loading its (code-split) runtime — keep this module data-only.
|
|
34
|
+
* The shell hands the chosen `id` back to the app at mount via `MangCore.launch`.
|
|
35
|
+
*/
|
|
36
|
+
interface MangCommand {
|
|
37
|
+
/** Unique within the app, e.g. "history", "export", "jwt". Becomes the `/app/<id>/<command>` segment. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Shown in the palette — the app owns its own copy (vi + en). */
|
|
40
|
+
title: LocalizedText;
|
|
41
|
+
/** Extra search terms beyond the title ("csv", "bảng công", "decode"). */
|
|
42
|
+
keywords?: string[];
|
|
43
|
+
/** Emoji for the palette row; falls back to the app icon. */
|
|
44
|
+
icon?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Surfaces this feature runs on. Omitted = ALL surfaces (the default — runs everywhere).
|
|
47
|
+
* Narrows within the app's `MangManifest.surfaces` envelope; a command should not claim a
|
|
48
|
+
* surface its app doesn't support. The shell will LATER use this to badge/disable unsupported
|
|
49
|
+
* features (an unavailable feature stays discoverable, never silently hidden) — for now it is a
|
|
50
|
+
* declared seam with no runtime gating. See `sot/Mang_Tech_Foundations` §2.1.
|
|
51
|
+
*/
|
|
52
|
+
surfaces?: Surface[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One screen in a mini-app's **main flow** (luồng chính) — the canonical happy-path a reviewer (and
|
|
56
|
+
* LATER a bot) walks to PROVE the real screens render and to AUTO-CAPTURE the store gallery/guide.
|
|
57
|
+
* Declared STATICALLY on the manifest (data-only, like `commands`) so the platform can read the flow
|
|
58
|
+
* WITHOUT loading the app's runtime.
|
|
59
|
+
*
|
|
60
|
+
* The contract a step asserts is a single DOM convention: when the screen is ready, the app MUST
|
|
61
|
+
* render an element carrying `data-mang-checkpoint="<step.checkpoint>"`. The walker navigates to the
|
|
62
|
+
* step (re-mounting with `launch: { command }`), waits for that element, screenshots, and records
|
|
63
|
+
* pass/fail. Populated screens come from the optional `MiniAppModule.prepareDemo` seed — steps do NOT
|
|
64
|
+
* simulate typing/clicking. See `sot/Mang_Tech_Foundations` §2.8.
|
|
65
|
+
*/
|
|
66
|
+
interface FlowStep {
|
|
67
|
+
/** Unique within the app, e.g. "home", "settle". Becomes the screenshot/file key. */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Step label shown in the generated guide (vi + en). */
|
|
70
|
+
title: LocalizedText;
|
|
71
|
+
/**
|
|
72
|
+
* A `MangCommand.id` to activate this screen via the launch seam. Omitted = the app's default
|
|
73
|
+
* screen on a plain mount (use for the first step / single-screen apps).
|
|
74
|
+
*/
|
|
75
|
+
command?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The app MUST render `[data-mang-checkpoint="<this>"]` when this screen is ready. This single DOM
|
|
78
|
+
* marker is the whole contract the walker waits on; a missing checkpoint fails the step.
|
|
79
|
+
*/
|
|
80
|
+
checkpoint: string;
|
|
81
|
+
/** Optional caption shown under the captured screenshot in the guide. */
|
|
82
|
+
caption?: LocalizedText;
|
|
83
|
+
/**
|
|
84
|
+
* Surfaces this step applies to. Omitted = ALL (default). Narrows within the app's `surfaces`
|
|
85
|
+
* envelope — lets a desktop-only screen be skipped when walking the web surface.
|
|
86
|
+
*/
|
|
87
|
+
surfaces?: Surface[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* An app's declared AI needs. DECLARATION-ONLY this phase (mirrors `surfaces`): the shell will
|
|
91
|
+
* LATER use it to badge / route / pre-warm / provision, but nothing gates on it now. This is the
|
|
92
|
+
* developer-facing contract that lets a creator TARGET the platform's AI seam ("I need a 'quality'
|
|
93
|
+
* power") instead of shipping their own model — local today, possibly remote LATER, zero app change.
|
|
94
|
+
* See `sot/Mang_Tech_Foundations` §2.4.
|
|
95
|
+
*/
|
|
96
|
+
interface MangAiDeclaration {
|
|
97
|
+
/** Capability tiers the app's AI features rely on. The shell may use this to recommend downloads. */
|
|
98
|
+
capabilities: AICapability[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* An app's declared remote-data needs. DECLARATION-ONLY this phase (mirrors `ai`/`surfaces`): the
|
|
102
|
+
* shell uses it to BADGE the app (☁️ cloud-backed, ⚡ realtime) and LATER to route/provision, but
|
|
103
|
+
* nothing gates on it at runtime. This is the developer-facing contract that lets a creator TARGET
|
|
104
|
+
* the platform's hosted-data seam ("my app keeps data on Măng's server / is multiplayer") instead
|
|
105
|
+
* of shipping their own backend — the implementation (polling today, realtime LATER) swaps with zero
|
|
106
|
+
* app change. See `sot/Mang_Tech_Foundations` §2.7.
|
|
107
|
+
*/
|
|
108
|
+
interface MangDataDeclaration {
|
|
109
|
+
/**
|
|
110
|
+
* The app coordinates many users on ONE shared dataset (a room shared by link). Drives the ⚡
|
|
111
|
+
* "Thời gian thực / nhiều người" badge. Omitted/false = private cross-device save only.
|
|
112
|
+
*/
|
|
113
|
+
shared?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* The app benefits from live updates (vs. occasional save/restore). Today every transport is
|
|
116
|
+
* near-live (poll); a future WebSocket transport makes it truly realtime — declaration is stable.
|
|
117
|
+
*/
|
|
118
|
+
realtime?: boolean;
|
|
119
|
+
}
|
|
120
|
+
/** A mini-app's identity + declared permissions + the features it exposes to the shell. */
|
|
121
|
+
interface MangManifest {
|
|
122
|
+
/** Stable slug, e.g. "chia-tien-nhom". Also the storage namespace. */
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
/** Emoji or asset reference for the launcher. */
|
|
126
|
+
icon: string;
|
|
127
|
+
version: string;
|
|
128
|
+
permissions: Permission[];
|
|
129
|
+
/** Features the app exposes to the host (searchable + launchable). Optional + additive. */
|
|
130
|
+
commands?: MangCommand[];
|
|
131
|
+
/**
|
|
132
|
+
* Surfaces the app supports overall (its envelope). Omitted = ALL surfaces. A `MangCommand`'s own
|
|
133
|
+
* `surfaces` narrows within this. See `sot/Mang_Tech_Foundations` §2.1.
|
|
134
|
+
*/
|
|
135
|
+
surfaces?: Surface[];
|
|
136
|
+
/**
|
|
137
|
+
* The app's **main flow** (luồng chính) — the canonical happy-path screens, in order, that the
|
|
138
|
+
* platform walks to verify the app renders and to auto-capture its gallery/guide. A standard part
|
|
139
|
+
* of the mini-app contract (declared in code, not just docs); see `sot/Mang_Tech_Foundations` §2.8.
|
|
140
|
+
* Optional in the type but REQUIRED by curation policy (validation warns when absent, will fail).
|
|
141
|
+
*/
|
|
142
|
+
flow?: FlowStep[];
|
|
143
|
+
/**
|
|
144
|
+
* Declared AI capabilities (developer-facing contract). Present only when `permissions` includes
|
|
145
|
+
* "ai". Declaration-only now (no runtime gate), like `surfaces`. See `sot/Mang_Tech_Foundations` §2.4.
|
|
146
|
+
*/
|
|
147
|
+
ai?: MangAiDeclaration;
|
|
148
|
+
/**
|
|
149
|
+
* Declared remote-data needs (developer-facing contract). Present only when `permissions` includes
|
|
150
|
+
* "data". Declaration-only now (drives badges), like `ai`. See `sot/Mang_Tech_Foundations` §2.7.
|
|
151
|
+
*/
|
|
152
|
+
data?: MangDataDeclaration;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* What the shell asks a mini-app to do at mount. Set when the app is opened via a command
|
|
156
|
+
* (palette, deep link `/app/<id>/<command>`); absent on a plain open. The app reads it once on
|
|
157
|
+
* mount and routes to that feature. A focused launch seam — distinct from the reserved
|
|
158
|
+
* `context?` (Tre SDK) seam.
|
|
159
|
+
*/
|
|
160
|
+
interface LaunchIntent {
|
|
161
|
+
/** The `MangCommand.id` the app should activate. */
|
|
162
|
+
command: string;
|
|
163
|
+
/** Optional extra arguments for the command (reserved; unused by current apps). */
|
|
164
|
+
params?: Record<string, string>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Per-app key/value storage. Local now (e.g. localStorage/IndexedDB), sync later —
|
|
168
|
+
* the app cannot tell the difference. Keys are scoped to the owning app.
|
|
169
|
+
*/
|
|
170
|
+
interface ScopedStorage {
|
|
171
|
+
get<T>(key: string): Promise<T | null>;
|
|
172
|
+
set<T>(key: string, val: T): Promise<void>;
|
|
173
|
+
list(prefix?: string): Promise<string[]>;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Măng brand design tokens, supplied by the shell (values live in `@mangtre/ui`).
|
|
177
|
+
* Palette per `sot/Mang_Art_Direction_v0.2.md` §5 — the Măng *product* identity
|
|
178
|
+
* ("Mầm số" — clean-modern, organic-bamboo), NOT the RumitX studio palette. The shell chrome wears
|
|
179
|
+
* these; a mini-app MAY use them but isn't required to (creator apps style
|
|
180
|
+
* themselves). The single shared thread with RumitX is `mangGreen`.
|
|
181
|
+
*/
|
|
182
|
+
interface ThemeTokens {
|
|
183
|
+
color: {
|
|
184
|
+
mangGreen: string;
|
|
185
|
+
shoot: string;
|
|
186
|
+
bamboo: string;
|
|
187
|
+
soil: string;
|
|
188
|
+
sun: string;
|
|
189
|
+
paper: string;
|
|
190
|
+
dark: string;
|
|
191
|
+
};
|
|
192
|
+
font: {
|
|
193
|
+
heading: string;
|
|
194
|
+
body: string;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Sign-in / identity client. Reserved seam. */
|
|
198
|
+
type IdentityClient = Record<string, never>;
|
|
199
|
+
/** Shared context/memory layer ("Tre SDK", a future bet — NOT `tre-mem`). Reserved seam. */
|
|
200
|
+
type TreContext = Record<string, never>;
|
|
201
|
+
/** Why local AI isn't usable right now — drives the app's degraded-mode copy. */
|
|
202
|
+
type AIUnavailableReason = "no-runtime" | "no-model" | "unknown";
|
|
203
|
+
/**
|
|
204
|
+
* What KIND of work a request needs — a device-/transport-neutral tier the resolver maps to a
|
|
205
|
+
* concrete model. The app expresses INTENT ("I need quality Vietnamese text"); it never names a
|
|
206
|
+
* model. The resolver (TS transport + Rust `ai.rs`) routes tier → model and DEGRADES to a smaller
|
|
207
|
+
* installed model rather than hard-failing. User-facing, each tier is a white-label "power"
|
|
208
|
+
* ("Năng lực AI") — the user never sees the tier id or the model. Researched local roles today:
|
|
209
|
+
* - "fast" → tiny router/classifier (qwen3:0.6b)
|
|
210
|
+
* - "balanced" → everyday default (qwen3:1.7b) ← the seam's default tier
|
|
211
|
+
* - "quality" → best text + strong Vietnamese (qwen3:4b-instruct-2507-q4_K_M)
|
|
212
|
+
* - "code" → code generation/editing (qwen2.5-coder:1.5b / 3b)
|
|
213
|
+
* - "reasoning" → deep/multi-step (qwen2.5 ~7B)
|
|
214
|
+
* Open by intent, NOT by model: a future remote transport maps the SAME tiers to its own models.
|
|
215
|
+
*/
|
|
216
|
+
type AICapability = "fast" | "balanced" | "quality" | "code" | "reasoning";
|
|
217
|
+
/** The tier assumed when a request omits `capability` — keeps `generate()` fully backward-compatible. */
|
|
218
|
+
declare const DEFAULT_AI_CAPABILITY: AICapability;
|
|
219
|
+
/**
|
|
220
|
+
* The user-facing "power" (năng lực AI) — the white-label face of an `AICapability`, presented
|
|
221
|
+
* WITHOUT naming a model or runtime. The setup/onboarding UI renders these chips instead of model
|
|
222
|
+
* ids; one power may be backed by several downloadable models (the resolver's table decides). Labels
|
|
223
|
+
* are localized because they're a primary user surface (Vietnamese-first).
|
|
224
|
+
*/
|
|
225
|
+
interface AIPower {
|
|
226
|
+
/** The capability tier this power represents. */
|
|
227
|
+
capability: AICapability;
|
|
228
|
+
/** Short user-facing name, e.g. { vi: "Trả lời nhanh", en: "Quick answers" }. */
|
|
229
|
+
label: LocalizedText;
|
|
230
|
+
/** One-line description of what this power does — no model/runtime jargon. */
|
|
231
|
+
description: LocalizedText;
|
|
232
|
+
/** Emoji/asset for the power chip. */
|
|
233
|
+
icon: string;
|
|
234
|
+
}
|
|
235
|
+
/** Whether local AI can run right now, and which models / powers are available. */
|
|
236
|
+
interface AIStatus {
|
|
237
|
+
/** True only when a runtime is reachable AND at least one model is pulled. */
|
|
238
|
+
ready: boolean;
|
|
239
|
+
/** Model ids the runtime reports (e.g. "llama3.2", "qwen2.5"); empty when none. */
|
|
240
|
+
models: string[];
|
|
241
|
+
/** Set when `ready` is false, so the app can tell the user exactly what to fix. */
|
|
242
|
+
reason?: AIUnavailableReason;
|
|
243
|
+
/**
|
|
244
|
+
* Which capability tiers can run RIGHT NOW given what's installed (every entry resolves to some
|
|
245
|
+
* model after degradation). Empty/absent when not ready. Lets the UI show available powers without
|
|
246
|
+
* leaking model ids. Optional → shells that don't populate it still type-check.
|
|
247
|
+
*/
|
|
248
|
+
capabilities?: AICapability[];
|
|
249
|
+
/**
|
|
250
|
+
* Where the ready AI runs. "local" = on-device (Ollama today; nothing leaves the device).
|
|
251
|
+
* "remote" is RESERVED for a LATER hosted transport — no remote impl ships now. Optional; absent
|
|
252
|
+
* is treated as "local" by consumers. This is the drop-in seam for the local/remote split.
|
|
253
|
+
*/
|
|
254
|
+
source?: "local" | "remote";
|
|
255
|
+
}
|
|
256
|
+
/** One text-generation request. The app owns prompt wording + any output parsing. */
|
|
257
|
+
interface AIGenerateRequest {
|
|
258
|
+
/** The user/content prompt. */
|
|
259
|
+
prompt: string;
|
|
260
|
+
/** Optional system instruction (role / format guidance). */
|
|
261
|
+
system?: string;
|
|
262
|
+
/** Ask the model to return strict JSON (enables the runtime's JSON mode where supported). */
|
|
263
|
+
json?: boolean;
|
|
264
|
+
/** Cooperative cancellation (e.g. the user navigates away mid-generation). */
|
|
265
|
+
signal?: AbortSignal;
|
|
266
|
+
/**
|
|
267
|
+
* What KIND of work this needs. The transport resolves it to a concrete model (tier-aware +
|
|
268
|
+
* device-aware: degrades to the best smaller installed model, never hard-fails). Omitted →
|
|
269
|
+
* `DEFAULT_AI_CAPABILITY` ("balanced"). The app NEVER names a model — that's the resolver's job.
|
|
270
|
+
*/
|
|
271
|
+
capability?: AICapability;
|
|
272
|
+
}
|
|
273
|
+
/** A completed generation. `text` is RAW model output — the app parses/validates it. */
|
|
274
|
+
interface AIGenerateResult {
|
|
275
|
+
text: string;
|
|
276
|
+
/** The model that actually answered (for display / a non-PII analytics dimension). */
|
|
277
|
+
model: string;
|
|
278
|
+
/** The tier this request resolved against (after any device degradation). Optional, display-only. */
|
|
279
|
+
capability?: AICapability;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* A model the shell recommends the user download — a curated, transport-owned whitelist (the app
|
|
283
|
+
* never hardcodes model ids). Small/fast variants come first so first-run generation is quick.
|
|
284
|
+
*/
|
|
285
|
+
interface AIModelOption {
|
|
286
|
+
/** The runtime's model id to pull, e.g. "llama3.2:3b". */
|
|
287
|
+
id: string;
|
|
288
|
+
/** Short display name, e.g. "Llama 3.2". */
|
|
289
|
+
label: string;
|
|
290
|
+
/** Human download-size hint, e.g. "~2 GB". */
|
|
291
|
+
sizeLabel: string;
|
|
292
|
+
/** The default pick the UI should highlight. */
|
|
293
|
+
recommended?: boolean;
|
|
294
|
+
/**
|
|
295
|
+
* The capability tier(s) this model best serves — lets setup group downloads by power
|
|
296
|
+
* ("install this to unlock the 'Chất lượng cao' power"). Optional + additive.
|
|
297
|
+
*/
|
|
298
|
+
capabilities?: AICapability[];
|
|
299
|
+
}
|
|
300
|
+
/** Progress of a model download. `percent` is 0..100 when the runtime reports byte totals. */
|
|
301
|
+
interface AIPullProgress {
|
|
302
|
+
/** Coarse phase label from the runtime (e.g. "downloading", "verifying"). */
|
|
303
|
+
status: string;
|
|
304
|
+
/** 0..100 when known; omitted while the runtime hasn't reported totals yet. */
|
|
305
|
+
percent?: number;
|
|
306
|
+
}
|
|
307
|
+
/** Options for a model download: progress callback + cooperative cancellation. */
|
|
308
|
+
interface AIPullOptions {
|
|
309
|
+
onProgress?: (progress: AIPullProgress) => void;
|
|
310
|
+
signal?: AbortSignal;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Local-first AI capability ("the brain on the user's own machine"). Present ONLY on a shell that
|
|
314
|
+
* can host a model runtime — today the Tauri desktop shell talking to a local **Ollama** server, so
|
|
315
|
+
* NOTHING leaves the device. Web / PWA / Zalo leave this `undefined`; guard at call sites:
|
|
316
|
+
* `sdk.ai?.generate(...)`. The app MUST degrade gracefully when absent (manual paths stay usable).
|
|
317
|
+
*
|
|
318
|
+
* **Transport-agnostic by design:** the implementation (local Ollama today; a hosted/metered
|
|
319
|
+
* provider is a LATER swap — see Roadmap) lives behind this interface, so the mini-app never
|
|
320
|
+
* changes. Text-in / text-out on purpose — prompt templates and output parsing belong to the app,
|
|
321
|
+
* not the seam. See `sot/Mang_Tech_Foundations` §2.4.
|
|
322
|
+
*
|
|
323
|
+
* The onboarding members (`recommendedModels` / `pull` / `openInstall`) are OPTIONAL: they let an
|
|
324
|
+
* app guide a first-run user to install the runtime + download a model in-app, but a future hosted
|
|
325
|
+
* transport (nothing to install) simply omits them. Guard with optional chaining.
|
|
326
|
+
*/
|
|
327
|
+
interface AIClient {
|
|
328
|
+
/** Probe the local runtime: is a model reachable right now? Cheap; safe to call on mount. */
|
|
329
|
+
status(): Promise<AIStatus>;
|
|
330
|
+
/** Generate text from a prompt. Rejects on failure (no runtime, timeout, model error). */
|
|
331
|
+
generate(req: AIGenerateRequest): Promise<AIGenerateResult>;
|
|
332
|
+
/** The curated download whitelist for this runtime (small/fast first). Absent on hosted transports. */
|
|
333
|
+
recommendedModels?(): AIModelOption[];
|
|
334
|
+
/**
|
|
335
|
+
* The user-facing powers ("năng lực AI") this transport can offer — the white-label face of its
|
|
336
|
+
* capability tiers. The setup UI renders these instead of model ids. Absent on transports that
|
|
337
|
+
* don't expose a power list. See `sot/Mang_Tech_Foundations` §2.4.
|
|
338
|
+
*/
|
|
339
|
+
powers?(): AIPower[];
|
|
340
|
+
/**
|
|
341
|
+
* Download a model into the local runtime, reporting progress. Resolves when the model is ready;
|
|
342
|
+
* rejects on failure or cancellation. Present only when the runtime supports in-app downloads.
|
|
343
|
+
*/
|
|
344
|
+
pull?(model: string, opts?: AIPullOptions): Promise<void>;
|
|
345
|
+
/** Open the runtime's install page in the OS browser (guided setup when no runtime is found). */
|
|
346
|
+
openInstall?(): Promise<void>;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A single usage event. NON-PII by contract: describe WHAT happened (a feature was used), never
|
|
350
|
+
* WHO did it or sensitive values (no names, no amounts, no free text). `props` carry small
|
|
351
|
+
* dimensions only — enums / counts / booleans — so the same event is safe to send anywhere a
|
|
352
|
+
* remote sink lands later.
|
|
353
|
+
*/
|
|
354
|
+
interface AnalyticsEvent {
|
|
355
|
+
/** Dotted name the emitter owns, e.g. "expense.added", "report.exported". */
|
|
356
|
+
name: string;
|
|
357
|
+
/** Non-PII dimensions only: small enums / counts / booleans. */
|
|
358
|
+
props?: Record<string, string | number | boolean>;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* What the shell's sink receives — an `AnalyticsEvent` plus provenance the emitter shouldn't set
|
|
362
|
+
* itself. `source` is `"shell"` for host events, or the mini-app id for app events (the SDK tags it).
|
|
363
|
+
*/
|
|
364
|
+
interface TrackedEvent extends AnalyticsEvent {
|
|
365
|
+
source: string;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Mini-app-facing analytics capability. Fire-and-forget: `track` never throws and never blocks the
|
|
369
|
+
* UI. Like the other optional seams, it's present only when the shell supplies a sink — guard with
|
|
370
|
+
* `sdk.analytics?.track(...)`. The default sink keeps events on-device (local-first); a remote sink
|
|
371
|
+
* (Cloudflare Workers Analytics Engine via the shell's `_worker.js`, or a vendor) is a LATER swap
|
|
372
|
+
* that changes zero lines here. See `sot/Mang_Tech_Foundations` §2.2.
|
|
373
|
+
*/
|
|
374
|
+
interface AnalyticsClient {
|
|
375
|
+
track(event: AnalyticsEvent): void;
|
|
376
|
+
}
|
|
377
|
+
/** The kind of payload a nearby device handed off — mirrors Smart Paste's own input space. */
|
|
378
|
+
type HandoffKind = "text" | "json" | "image";
|
|
379
|
+
/**
|
|
380
|
+
* One payload handed off from a nearby device on the same network. NON-PII by contract:
|
|
381
|
+
* `sourceDevice` is a coarse display label ("iPhone"), never an id / IP / account. The envelope
|
|
382
|
+
* is **versioned** (`v`) so a transport can evolve its shape without breaking older receivers
|
|
383
|
+
* (cf. the chia-tien-nhom share-link wire v1/v2). This is a ONE-SHOT handoff, NOT live sync.
|
|
384
|
+
*/
|
|
385
|
+
interface HandoffPayload {
|
|
386
|
+
/** Envelope version. Bump when the shape changes; receivers validate it. */
|
|
387
|
+
v: 1;
|
|
388
|
+
kind: HandoffKind;
|
|
389
|
+
/** `text`/`json`: the raw string. `image`: a `data:image/<mime>;base64,...` data URL. */
|
|
390
|
+
data: string;
|
|
391
|
+
/** Short human label for the sender ("iPhone", "Pixel"). Display-only, non-PII. */
|
|
392
|
+
sourceDevice: string;
|
|
393
|
+
/** Epoch ms when the receiving shell got it (stamped by the sink). */
|
|
394
|
+
receivedAt: number;
|
|
395
|
+
}
|
|
396
|
+
/** A subscriber the mini-app registers to receive handed-off payloads. */
|
|
397
|
+
type HandoffListener = (payload: HandoffPayload) => void;
|
|
398
|
+
/**
|
|
399
|
+
* How a sender connects — rendered by the receiver UI, NOT assumed to be a LAN URL. Today's LAN
|
|
400
|
+
* transport encodes a `http://<lan-ip>:<port>/h#t=<token>` URL as a QR (`mode: "lan-qr"`); a future
|
|
401
|
+
* WebRTC transport could instead surface a short pairing `code`. Transport-neutral so the seam
|
|
402
|
+
* outlives any single mechanism.
|
|
403
|
+
*/
|
|
404
|
+
interface HandoffPairing {
|
|
405
|
+
/** Open union so new transports add modes additively without a breaking change. */
|
|
406
|
+
mode: "lan-qr" | string;
|
|
407
|
+
/** Opaque payload to render as a QR (LAN: the URL + token). */
|
|
408
|
+
qr?: string;
|
|
409
|
+
/** Human-readable pairing code (e.g. WebRTC signaling). Reserved — unused by the LAN transport. */
|
|
410
|
+
code?: string;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Receive-only inbound channel for device handoff ("paste from a nearby device"). Present ONLY
|
|
414
|
+
* when the shell can host a transport — today the Tauri desktop shell + a local LAN server.
|
|
415
|
+
* Web / PWA / Zalo builds leave this `undefined`; guard at call sites: `sdk.handoff?.onPayload(...)`.
|
|
416
|
+
*
|
|
417
|
+
* **Transport-agnostic by design:** the implementation (LAN-HTTP today, WebRTC/BLE/relay later)
|
|
418
|
+
* swaps behind this interface, so the mini-app never changes. This is a ONE-SHOT handoff seam,
|
|
419
|
+
* NOT live multi-device sync (sync stays parked — see `sot/Mang_Tech_Foundations` §2.3 / Roadmap).
|
|
420
|
+
*/
|
|
421
|
+
interface HandoffInbox {
|
|
422
|
+
/** Subscribe to incoming payloads. Returns an unsubscribe fn. Fires once per payload. */
|
|
423
|
+
onPayload(listener: HandoffListener): () => void;
|
|
424
|
+
/** Current connect-descriptor for the sender to scan/enter, or `null` until the transport is ready. */
|
|
425
|
+
pairing(): HandoffPairing | null;
|
|
426
|
+
/**
|
|
427
|
+
* Subscribe to "a sender device connected" signals (a nearby device opened the sender page) so the
|
|
428
|
+
* receiver can show a live "📱 connected" state. Carries the coarse, non-PII device label. Optional:
|
|
429
|
+
* present only when the transport reports sender presence. Returns an unsubscribe fn.
|
|
430
|
+
*/
|
|
431
|
+
onSenderSeen?(listener: (device: string) => void): () => void;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* How a room is shared. `"private"` = one creator/device's cross-device save (a personal cloud
|
|
435
|
+
* backup); `"shared"` = many users coordinate on ONE dataset via a room link (the multiplayer case).
|
|
436
|
+
* The shell injects the owning app id server-side, so a room is always scoped to its app.
|
|
437
|
+
*/
|
|
438
|
+
type DataMode = "private" | "shared";
|
|
439
|
+
/** A room the app is currently joined to. The `roomKey` capability lives in the SHELL, never here. */
|
|
440
|
+
interface DataRoomInfo {
|
|
441
|
+
/** Public id (safe to put in a share link). */
|
|
442
|
+
roomId: string;
|
|
443
|
+
mode: DataMode;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Whether the data seam can talk to the server right now, and over which transport. `transport`
|
|
447
|
+
* is informational only — the app behaves identically whether it's near-live polling ("poll") or
|
|
448
|
+
* a future WebSocket ("ws"). Absent/`ready:false` → the app should fall back to local-only paths.
|
|
449
|
+
*/
|
|
450
|
+
interface DataStatus {
|
|
451
|
+
ready: boolean;
|
|
452
|
+
reason?: string;
|
|
453
|
+
transport?: "poll" | "ws";
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* One change observed in the joined room — a key was written (`value` set) or deleted (`value: null`).
|
|
457
|
+
* `rev` is the room-wide monotonic revision after the change (also the optimistic-concurrency token).
|
|
458
|
+
*/
|
|
459
|
+
interface DataChange<T = unknown> {
|
|
460
|
+
key: string;
|
|
461
|
+
value: T | null;
|
|
462
|
+
rev: number;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Hosted key/value data with optional live sharing — the SERVER-backed sibling of `ScopedStorage`.
|
|
466
|
+
* Present ONLY when the shell supplies a data transport (web/desktop with the platform API reachable
|
|
467
|
+
* + the app holds the `"data"` permission). Web/PWA before this lands, or unsupported surfaces, leave
|
|
468
|
+
* it `undefined`; guard at call sites: `sdk.data?.set(...)`. The app MUST degrade to local-only when
|
|
469
|
+
* absent — the seam is additive, never load-bearing for the core experience.
|
|
470
|
+
*
|
|
471
|
+
* **A room is a capability, not an account.** `createRoom` returns a `roomId` + an unguessable
|
|
472
|
+
* `roomKey`; whoever holds the key can read/write/subscribe (the same trust model as today's
|
|
473
|
+
* `#g1=`/`#tkb=` share links, now server-backed and live). No end-user login, no PII — an anonymous
|
|
474
|
+
* device id tags writes for presence only. The SHELL stores the `roomKey` and brokers every call;
|
|
475
|
+
* the app passes only keys + values. The app can only ever touch ITS OWN app's rooms (the host
|
|
476
|
+
* injects the app id).
|
|
477
|
+
*
|
|
478
|
+
* **Transport-agnostic by design:** near-live polling today, WebSocket realtime + presence LATER —
|
|
479
|
+
* the implementation swaps behind this interface with zero app change. Values are JSON-serializable;
|
|
480
|
+
* the app owns its own data schema. See `sot/Mang_Tech_Foundations` §2.7.
|
|
481
|
+
*/
|
|
482
|
+
interface DataClient {
|
|
483
|
+
/** Can the seam reach the server right now? Cheap; safe to call on mount. */
|
|
484
|
+
status(): Promise<DataStatus>;
|
|
485
|
+
/**
|
|
486
|
+
* Create a fresh room and join it. Returns the public `roomId` plus the secret `roomKey` capability
|
|
487
|
+
* (put both in a share link to invite others). The shell remembers the key for subsequent calls.
|
|
488
|
+
*/
|
|
489
|
+
createRoom(opts?: {
|
|
490
|
+
mode?: DataMode;
|
|
491
|
+
}): Promise<{
|
|
492
|
+
roomId: string;
|
|
493
|
+
roomKey: string;
|
|
494
|
+
mode: DataMode;
|
|
495
|
+
}>;
|
|
496
|
+
/** Join an existing room with its `roomKey` (e.g. from an opened share link). */
|
|
497
|
+
joinRoom(roomId: string, roomKey: string): Promise<DataRoomInfo>;
|
|
498
|
+
/** The room the app is joined to, or `null` before any create/join. */
|
|
499
|
+
current(): DataRoomInfo | null;
|
|
500
|
+
/** Read a key from the joined room. `null` when the key is absent (no room joined → rejects). */
|
|
501
|
+
get<T>(key: string): Promise<{
|
|
502
|
+
value: T | null;
|
|
503
|
+
rev: number;
|
|
504
|
+
} | null>;
|
|
505
|
+
/**
|
|
506
|
+
* Write a key. `expectedRev` enables optimistic concurrency: pass the `rev` you last saw and the
|
|
507
|
+
* write rejects with a conflict if someone else wrote in between (omit to force-write). Resolves
|
|
508
|
+
* with the new room-wide `rev`.
|
|
509
|
+
*/
|
|
510
|
+
set<T>(key: string, val: T, expectedRev?: number): Promise<{
|
|
511
|
+
rev: number;
|
|
512
|
+
}>;
|
|
513
|
+
/** Delete a key (optionally guarded by `expectedRev`). */
|
|
514
|
+
remove(key: string, expectedRev?: number): Promise<{
|
|
515
|
+
rev: number;
|
|
516
|
+
}>;
|
|
517
|
+
/** List keys in the joined room (optionally by prefix) with each key's current `rev`. */
|
|
518
|
+
list(prefix?: string): Promise<{
|
|
519
|
+
key: string;
|
|
520
|
+
rev: number;
|
|
521
|
+
}[]>;
|
|
522
|
+
/**
|
|
523
|
+
* Revoke the joined room: its capability stops working for everyone and live subscribers are cut. Use
|
|
524
|
+
* to "stop sharing". The app should drop its local room pointer after this. No-op if no room is joined.
|
|
525
|
+
*/
|
|
526
|
+
revoke(): Promise<void>;
|
|
527
|
+
/**
|
|
528
|
+
* Rotate the joined room's capability (e.g. a shared link leaked): returns a NEW `roomKey` to re-share;
|
|
529
|
+
* old links stop working and live subscribers are cut. The shell keeps using the room with the new key.
|
|
530
|
+
*/
|
|
531
|
+
rotateKey(): Promise<{
|
|
532
|
+
roomKey: string;
|
|
533
|
+
}>;
|
|
534
|
+
/**
|
|
535
|
+
* Subscribe to changes in the joined room. Fires once per observed change. Returns an unsubscribe
|
|
536
|
+
* fn. Near-live (poll) today; a future transport makes it instantaneous — the callback shape is
|
|
537
|
+
* identical, so the app never changes.
|
|
538
|
+
*/
|
|
539
|
+
subscribe<T>(listener: (change: DataChange<T>) => void): () => void;
|
|
540
|
+
}
|
|
541
|
+
/** Everything the shell hands a mini-app at mount time. */
|
|
542
|
+
interface MangCore {
|
|
543
|
+
storage: ScopedStorage;
|
|
544
|
+
theme: ThemeTokens;
|
|
545
|
+
/**
|
|
546
|
+
* Active UI language, chosen by the shell. Ambient runtime config (always present, like
|
|
547
|
+
* `theme`) — NOT a gated `Permission`. A mini-app SHOULD localize against it (e.g. via the
|
|
548
|
+
* `@mangtre/i18n` helper); the shell re-mounts the app when the user switches language.
|
|
549
|
+
*/
|
|
550
|
+
locale: Locale;
|
|
551
|
+
/**
|
|
552
|
+
* The feature the shell wants active for THIS mount (set when opened via a command/deep link).
|
|
553
|
+
* Ambient like `theme`/`locale`, not a gated `Permission`. The shell re-mounts the app when the
|
|
554
|
+
* launch command changes, so reading it once on mount is enough.
|
|
555
|
+
*/
|
|
556
|
+
launch?: LaunchIntent;
|
|
557
|
+
/**
|
|
558
|
+
* Local-first AI. Present ONLY on a shell that can host a model runtime — today the Tauri desktop
|
|
559
|
+
* shell + a local Ollama server (nothing leaves the device). Web / PWA / Zalo leave it `undefined`.
|
|
560
|
+
* Guard at call sites: `sdk.ai?.generate(...)`; the app degrades to manual paths when absent.
|
|
561
|
+
*/
|
|
562
|
+
ai?: AIClient;
|
|
563
|
+
identity?: IdentityClient;
|
|
564
|
+
context?: TreContext;
|
|
565
|
+
/**
|
|
566
|
+
* Usage analytics. Present only when the shell supplies a sink. Local-first today (events buffer
|
|
567
|
+
* on-device); remote sink is a LATER swap. Guard at call sites: `sdk.analytics?.track(...)`.
|
|
568
|
+
*/
|
|
569
|
+
analytics?: AnalyticsClient;
|
|
570
|
+
/**
|
|
571
|
+
* Inbound device handoff ("paste from a nearby device"). Present ONLY on a shell that can host a
|
|
572
|
+
* transport — today the Tauri desktop shell (LAN server). Receive-only, one-shot (NOT sync), and
|
|
573
|
+
* transport-agnostic. Guard at call sites: `sdk.handoff?.onPayload(...)`. See Tech Foundations §2.3.
|
|
574
|
+
*/
|
|
575
|
+
handoff?: HandoffInbox;
|
|
576
|
+
/**
|
|
577
|
+
* Hosted data + live rooms ("Phòng sống") — the server-backed sibling of `storage`. Present ONLY
|
|
578
|
+
* when the shell supplies a data transport and the app holds the `"data"` permission. Lets a
|
|
579
|
+
* mini-app keep data on Măng's server and (with a room link) share ONE live dataset across many
|
|
580
|
+
* users — no backend, no end-user login, capability-based. Guard at call sites: `sdk.data?.set(...)`;
|
|
581
|
+
* the app degrades to local-only when absent. See Tech Foundations §2.7.
|
|
582
|
+
*/
|
|
583
|
+
data?: DataClient;
|
|
584
|
+
}
|
|
585
|
+
/** Tear-down handle returned by a mini-app's `mount`. */
|
|
586
|
+
type Unmount = () => void;
|
|
587
|
+
/** The single entry every mini-app exports. */
|
|
588
|
+
type Mount = (root: HTMLElement, sdk: MangCore) => Unmount;
|
|
589
|
+
/**
|
|
590
|
+
* Optional hook a mini-app exports to seed DEMO state before its main flow is walked. Runs ONCE
|
|
591
|
+
* (before the first flow step) against the same `sdk.storage` the app reads on mount, so the
|
|
592
|
+
* subsequent re-mounts show populated screens for screenshots. Reuse the app's existing sample/seed
|
|
593
|
+
* logic. No-op / absent ⇒ the flow walks against whatever a fresh mount shows. See Tech Foundations §2.8.
|
|
594
|
+
*/
|
|
595
|
+
type PrepareDemo = (sdk: MangCore) => Promise<void>;
|
|
596
|
+
/** Shape of a mini-app module loaded by the runtime. */
|
|
597
|
+
interface MiniAppModule {
|
|
598
|
+
manifest: MangManifest;
|
|
599
|
+
mount: Mount;
|
|
600
|
+
/** Seeds demo state for the flow walker; see {@link PrepareDemo}. */
|
|
601
|
+
prepareDemo?: PrepareDemo;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Tiny runtime guards for narrowing **untrusted JSON at a boundary** into typed values.
|
|
606
|
+
*
|
|
607
|
+
* A mini-app owns its own data schema (Măng stores opaque JSON — see {@link ScopedStorage} and
|
|
608
|
+
* `DataClient`). But data coming *back in* — a live-room snapshot another device wrote, a decoded
|
|
609
|
+
* share link, an old value with a since-changed shape — is semi-trusted input. Validate it before
|
|
610
|
+
* trusting it, or risk corrupting local state (or worse, an injection if the value reaches the DOM).
|
|
611
|
+
*
|
|
612
|
+
* These helpers follow the codebase's existing defensive style: **return `undefined` on anything
|
|
613
|
+
* malformed** (never throw), so a caller drops a bad payload instead of crashing. They are
|
|
614
|
+
* dependency-free on purpose — mini-app bundles are size-capped and statically scanned, so pulling a
|
|
615
|
+
* full schema library (zod, etc.) is the wrong trade here. Compose them to validate a whole record:
|
|
616
|
+
*
|
|
617
|
+
* ```ts
|
|
618
|
+
* import { isRecord, asString, asFiniteNumber, asArrayOf } from "@mangtre/core";
|
|
619
|
+
*
|
|
620
|
+
* interface Todo { id: string; text: string; done: boolean }
|
|
621
|
+
*
|
|
622
|
+
* function parseTodo(v: unknown): Todo | undefined {
|
|
623
|
+
* if (!isRecord(v)) return undefined;
|
|
624
|
+
* const id = asString(v.id);
|
|
625
|
+
* const text = asString(v.text);
|
|
626
|
+
* const done = asBoolean(v.done);
|
|
627
|
+
* if (id === undefined || text === undefined || done === undefined) return undefined;
|
|
628
|
+
* return { id, text, done };
|
|
629
|
+
* }
|
|
630
|
+
*
|
|
631
|
+
* const todos = asArrayOf(incoming, parseTodo); // undefined if ANY item is malformed
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
/** True when `v` is a non-null, non-array object — safe to index its keys as `unknown`. */
|
|
635
|
+
declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
636
|
+
/** A string, else `undefined`. */
|
|
637
|
+
declare function asString(v: unknown): string | undefined;
|
|
638
|
+
/** A finite number (rejects `NaN` and `±Infinity`), else `undefined`. */
|
|
639
|
+
declare function asFiniteNumber(v: unknown): number | undefined;
|
|
640
|
+
/** A boolean, else `undefined`. */
|
|
641
|
+
declare function asBoolean(v: unknown): boolean | undefined;
|
|
642
|
+
/**
|
|
643
|
+
* An array whose every item parses with `item`, else `undefined`. Short-circuits: if any item
|
|
644
|
+
* returns `undefined`, the whole array is rejected (a partially-valid list is still malformed
|
|
645
|
+
* input). Returns a fresh array; the input is never mutated.
|
|
646
|
+
*/
|
|
647
|
+
declare function asArrayOf<T>(v: unknown, item: (x: unknown) => T | undefined): T[] | undefined;
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Thrown when a write fails because the device storage quota is exhausted. Mini-apps
|
|
651
|
+
* can catch this to show a "storage full" message instead of a generic failure. (Other
|
|
652
|
+
* write errors propagate as-is.)
|
|
653
|
+
*/
|
|
654
|
+
declare class StorageQuotaError extends Error {
|
|
655
|
+
constructor(message?: string);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Monolith implementation of {@link ScopedStorage} over `localStorage`.
|
|
659
|
+
* Keys are namespaced per app so mini-apps cannot read each other's data.
|
|
660
|
+
*
|
|
661
|
+
* This is the "local now" backing. Swapping to a synced backend later means
|
|
662
|
+
* replacing this factory — mini-apps are unaffected.
|
|
663
|
+
*/
|
|
664
|
+
declare function createScopedLocalStorage(appId: string): ScopedStorage;
|
|
665
|
+
|
|
666
|
+
interface MonolithSdkOptions {
|
|
667
|
+
manifest: MangManifest;
|
|
668
|
+
theme: ThemeTokens;
|
|
669
|
+
locale: Locale;
|
|
670
|
+
/** Feature to activate on mount (set when opened via a command/deep link). */
|
|
671
|
+
launch?: LaunchIntent;
|
|
672
|
+
/**
|
|
673
|
+
* Low-level analytics sink supplied by the shell. Absent = the `analytics` seam stays dormant
|
|
674
|
+
* (no-op via optional chaining). The SDK wraps it into the mini-app's `AnalyticsClient`, tagging
|
|
675
|
+
* each event's `source` with the app id so the app never sets provenance itself.
|
|
676
|
+
*/
|
|
677
|
+
track?: (event: TrackedEvent) => void;
|
|
678
|
+
/**
|
|
679
|
+
* Inbound device-handoff channel supplied by the shell. Absent = the `handoff` seam stays dormant
|
|
680
|
+
* (no-op via optional chaining). Receive-only and transport-agnostic; the SDK passes it through
|
|
681
|
+
* unchanged (no per-app tagging — dev-tools-only this phase). Present only on the desktop shell.
|
|
682
|
+
*/
|
|
683
|
+
handoff?: HandoffInbox;
|
|
684
|
+
/**
|
|
685
|
+
* Local-first AI client supplied by the shell. Absent = the `ai` seam stays dormant (no-op via
|
|
686
|
+
* optional chaining). Transport-agnostic (local Ollama today); the SDK passes it through unchanged.
|
|
687
|
+
* Present only on a shell that can host a model runtime — today the Tauri desktop shell.
|
|
688
|
+
*/
|
|
689
|
+
ai?: AIClient;
|
|
690
|
+
/**
|
|
691
|
+
* Hosted-data client supplied by the shell. Absent = the `data` seam stays dormant (no-op via
|
|
692
|
+
* optional chaining). Transport-agnostic (near-live polling today, WebSocket realtime later); the
|
|
693
|
+
* SDK passes it through unchanged. The shell builds it per-app (it already knows the app id), so
|
|
694
|
+
* cross-app isolation is enforced host-side. Present only when the platform API is reachable AND
|
|
695
|
+
* the app declared the `"data"` permission.
|
|
696
|
+
*/
|
|
697
|
+
data?: DataClient;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Build the current (monolith) `MangCore` for a mini-app. Today this wires up
|
|
701
|
+
* local storage + injected theme. Later, a sandbox bridge produces the same
|
|
702
|
+
* shape — so mini-apps never change.
|
|
703
|
+
*/
|
|
704
|
+
declare function createMonolithSdk(opts: MonolithSdkOptions): MangCore;
|
|
705
|
+
|
|
706
|
+
export { type AICapability, type AIClient, type AIGenerateRequest, type AIGenerateResult, type AIModelOption, type AIPower, type AIPullOptions, type AIPullProgress, type AIStatus, type AIUnavailableReason, type AnalyticsClient, type AnalyticsEvent, DEFAULT_AI_CAPABILITY, type DataChange, type DataClient, type DataMode, type DataRoomInfo, type DataStatus, type FlowStep, type HandoffInbox, type HandoffKind, type HandoffListener, type HandoffPairing, type HandoffPayload, type IdentityClient, type LaunchIntent, type Locale, type LocalizedText, type MangAiDeclaration, type MangCommand, type MangCore, type MangDataDeclaration, type MangManifest, type MiniAppModule, type MonolithSdkOptions, type Mount, type Permission, type PrepareDemo, type ScopedStorage, StorageQuotaError, type Surface, type ThemeTokens, type TrackedEvent, type TreContext, type Unmount, asArrayOf, asBoolean, asFiniteNumber, asString, createMonolithSdk, createScopedLocalStorage, isRecord };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var DEFAULT_AI_CAPABILITY = "balanced";
|
|
3
|
+
|
|
4
|
+
// src/guards.ts
|
|
5
|
+
function isRecord(v) {
|
|
6
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7
|
+
}
|
|
8
|
+
function asString(v) {
|
|
9
|
+
return typeof v === "string" ? v : void 0;
|
|
10
|
+
}
|
|
11
|
+
function asFiniteNumber(v) {
|
|
12
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
13
|
+
}
|
|
14
|
+
function asBoolean(v) {
|
|
15
|
+
return typeof v === "boolean" ? v : void 0;
|
|
16
|
+
}
|
|
17
|
+
function asArrayOf(v, item) {
|
|
18
|
+
if (!Array.isArray(v)) return void 0;
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const x of v) {
|
|
21
|
+
const parsed = item(x);
|
|
22
|
+
if (parsed === void 0) return void 0;
|
|
23
|
+
out.push(parsed);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/storage.ts
|
|
29
|
+
var StorageQuotaError = class extends Error {
|
|
30
|
+
constructor(message = "Local storage quota exceeded") {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "StorageQuotaError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function isQuotaError(e) {
|
|
36
|
+
return e instanceof DOMException && (e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED" || e.code === 22 || e.code === 1014);
|
|
37
|
+
}
|
|
38
|
+
function createScopedLocalStorage(appId) {
|
|
39
|
+
const ns = `mang:${appId}:`;
|
|
40
|
+
return {
|
|
41
|
+
async get(key) {
|
|
42
|
+
const raw = localStorage.getItem(ns + key);
|
|
43
|
+
if (raw === null) return null;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async set(key, val) {
|
|
51
|
+
try {
|
|
52
|
+
localStorage.setItem(ns + key, JSON.stringify(val));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (isQuotaError(e)) throw new StorageQuotaError();
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
async list(prefix = "") {
|
|
59
|
+
const full = ns + prefix;
|
|
60
|
+
const keys = [];
|
|
61
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
62
|
+
const k = localStorage.key(i);
|
|
63
|
+
if (k?.startsWith(full)) {
|
|
64
|
+
keys.push(k.slice(ns.length));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return keys;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/sdk.ts
|
|
73
|
+
function createMonolithSdk(opts) {
|
|
74
|
+
const track = opts.track;
|
|
75
|
+
return {
|
|
76
|
+
storage: createScopedLocalStorage(opts.manifest.id),
|
|
77
|
+
theme: opts.theme,
|
|
78
|
+
locale: opts.locale,
|
|
79
|
+
// `launch` is only set on the property when present, so a plain open leaves it `undefined`.
|
|
80
|
+
...opts.launch ? { launch: opts.launch } : {},
|
|
81
|
+
// `analytics` only when the shell supplied a sink; events are tagged with the app id.
|
|
82
|
+
...track ? { analytics: { track: (e) => track({ ...e, source: opts.manifest.id }) } } : {},
|
|
83
|
+
// `handoff` only when the shell can host a transport (desktop); passed through unchanged.
|
|
84
|
+
...opts.handoff ? { handoff: opts.handoff } : {},
|
|
85
|
+
// `ai` only when the shell can host a model runtime (desktop + local Ollama); passed through unchanged.
|
|
86
|
+
...opts.ai ? { ai: opts.ai } : {},
|
|
87
|
+
// `data` only when the shell supplies a hosted-data transport; built per-app (app-scoped server-side).
|
|
88
|
+
...opts.data ? { data: opts.data } : {}
|
|
89
|
+
// identity / context: reserved seams, not provided yet.
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
DEFAULT_AI_CAPABILITY,
|
|
94
|
+
StorageQuotaError,
|
|
95
|
+
asArrayOf,
|
|
96
|
+
asBoolean,
|
|
97
|
+
asFiniteNumber,
|
|
98
|
+
asString,
|
|
99
|
+
createMonolithSdk,
|
|
100
|
+
createScopedLocalStorage,
|
|
101
|
+
isRecord
|
|
102
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mangtre/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"tsup": "^8.3.5",
|
|
21
|
+
"vitest": "^2.1.8"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
}
|
|
28
|
+
}
|