window.Magic API — HTML Micro-App Guide
How to Use This Document
- API signatures & constraints → this document
- App manifest & permission declarations →
app.json
- TiptapJSON & @mention structures → references/tiptap-json-format.md
- Complete HTML examples → references/complete-examples.md
Important Constraints
All window.Magic.* APIs are pre-injected — no imports needed. External CDN allowed.
File paths are relative to app root (index.html dir) by default. ../ is forbidden. Use leading-slash paths such as "/shared/data.json" to access project-root files. Writing, deleting, moving, or renaming files outside the app root triggers host confirmation.
window.Magic.llm tokens hosted; no api_key in HTML.
No inline event handlers — use addEventListener. For buttons rendered by innerHTML, bind one delegated listener on a stable container and use data-action/data-id.
LLM calls must include model selector UI unless user specifies model. Default "auto".
Complex file-based AI → use createTopicAndSend + @file + companion skill. Simple → readFile + llm.chat/stream.
High-risk APIs are permission-gated — new HTML micro-apps must declare requested scopes in app.json.permissions.scopes. The host asks the user to approve high-risk runtime calls for a limited duration.
User info is privacy-gated — window.Magic.user.getInfo() returns only name and avatar by default. Sensitive fields require a matching permission declaration, a runtime getInfo({ scopes, reason }) request, and user confirmation.
Use app.json as the micro-app manifest — every new HTML micro-app folder should include app.json next to index.html. Put type, name, entry, anonymous, file aliases, watch hints, and permissions there. Also generate a minimal magic.project.js display bridge that mirrors only version/type/name/entry/icon; do not put anonymous, permissions, files, watch, or business state in magic.project.js.
{
"version": "1.0.0",
"type": "micro-app",
"name": "App Name",
"entry": "index.html",
"anonymous": false,
"files": {},
"watch": [],
"permissions": {
"scopes": [],
"reason": ""
}
}
Administrator page access is runtime-controlled — when an app has administrator-only pages, put window.MagicAppConfig.admin_pages in the shared app.js and call window.Magic.db.getProjectAdminAccess() before loading each listed page. The result is based on the real logged-in user; a share token is only an access proof and is never a user identity.
HTML Interaction Safety
Generated micro-app controls must be wired through real JavaScript listeners, not HTML event attributes.
- Do not generate
onclick, onchange, oninput, onsubmit, or other inline event attributes.
- For lists, cards, table rows, and menus rendered with
innerHTML, use event delegation: container.addEventListener("click", handler) and buttons such as <button data-action="edit" data-id="...">.
- Do not attach action functions to
window just to make inline event handlers work.
- If using
new FormData(form), every value read with formData.get("field") must have a matching name="field" on the input/select/textarea. Having only id="field" is not enough.
- Before calling
.trim(), normalize possibly missing form values, for example String(formData.get("title") || "").trim().
- If a form is read by DOM IDs instead, use
.value consistently and do not mix it with FormData.get() for unnamed controls.
1. File System (window.Magic.fs)
readFile(path) → Promise<string>
const raw = await window.Magic.fs.readFile("data/tasks/20260624153000__open__a8f3k2__follow-up.json");
const task = JSON.parse(raw);
path: string — relative to app root. Max 5 MB; rejects if not found.
writeFile(path, content) → Promise<void>
await window.Magic.fs.writeFile(
"data/tasks/20260624153000__open__a8f3k2__follow-up.json",
JSON.stringify(record, null, 2),
);
// Binary (up to 500 MB):
await window.Magic.fs.writeFile("data/large.bin", blob);
content: string | Blob | ArrayBuffer. String max 5 MB. Auto-creates dirs. ../ blocked.
⚠️ Paths relative to index.html dir, NOT workspace root.
File Paths and Project-Root Access
By default, relative window.Magic.fs.* paths resolve inside the app folder next to index.html. Use a leading slash for project-root paths. Project-root reads require fs.project.read; project-root writes/deletes/moves/renames require fs.project.write plus a host path confirmation for each destructive operation.
Path rules:
"data/config.json" -> app root, e.g. my-app/data/config.json.
"/shared/config.json" -> project root.
"/" lists project-root entries.
../ remains blocked in all scopes.
- Reading project-root file contents or temporary URLs requires
fs.project.read.
- Writing, deleting, moving, or renaming files outside the app root requires
fs.project.write, then triggers host path confirmation and may be rejected by the user.
listFiles("/") and listDir("/") are not gated in the current version, but do not depend on them for sensitive directory discovery.
listFiles(dir?) → Promise<string[]>
const files = await window.Magic.fs.listFiles("data/");
- Compatibility API. It returns direct child names only. Prefer
listDir() for new list UIs.
listDir(dir?) → Promise<Array<{name,path,isDirectory,updatedAt?}>>
const entries = await window.Magic.fs.listDir("data/tasks/");
entries
.map((entry) => parseRecordFileName(entry.name))
.filter(Boolean)
.sort((a, b) => b.sortKey.localeCompare(a.sortKey));
- Returns direct children only. It does not read file contents.
- Use it for list pages. Read the JSON detail only when the user opens, edits, or analyzes one record.
path is usable with readFile, writeFile, deleteFile, moveFile, and renameFile.
getFileUrl(path) → Promise<string>
const imageUrl = await window.Magic.fs.getFileUrl("assets/chart.png");
document.getElementById("preview").src = imageUrl;
- Returns a temporary browser-accessible URL for an existing workspace file.
- Use it for previews,
<img>, <audio>, <video>, download links, or libraries that need a URL instead of file text.
- It does not download the file by itself. Use
window.Magic.project.downloadFiles(paths) when the user action should trigger a browser download.
- Rejects if the file is missing or the path is invalid.
../ blocked.
deleteFile(path) → Promise<void>
await window.Magic.fs.deleteFile("data/temp.json");
- Rejects if file not found.
../ blocked.
deleteDir(path) → Promise<void>
await window.Magic.fs.deleteDir("temp/");
- Recursively deletes all files and subdirectories. Cannot delete app root or project root. Rejects if dir not found.
../ blocked.
moveFile(path, targetDir) → Promise<void>
await window.Magic.fs.moveFile("data/old.json", "archive/");
- Moves a file or directory to the specified target parent directory. Rejects if source file or target directory not found.
../ blocked.
renameFile(path, newName) → Promise<void>
await window.Magic.fs.renameFile("data/draft.txt", "final.txt");
- Renames a file or directory.
newName is just the new name (no path separators). Rejects if file not found. ../ blocked.
watchFile(path, cb) → () => void
const unwatch = window.Magic.fs.watchFile("data/orders.json", async (e) => {
const fresh = JSON.parse(await window.Magic.fs.readFile("data/orders.json"));
renderTable(fresh);
});
- Polls ~3s; max 10 watched paths per app. Call returned fn to stop.
watchDir(dir, cb) → () => void
const unwatch = window.Magic.fs.watchDir("data/tasks/", (event) => {
// renameFile that changes projection appears as removed + added.
// Use parseRecordFileName(name).shortId to match the same record.
renderList(event.entries);
});
- Not a real-time filesystem watcher. It compares refreshed host attachment snapshots after the existing attachment polling or
Update_Attachments refresh.
- Watches direct child additions and removals only. File content changes continue to use
watchFile().
- Callback payload:
{ dir, timestamp, added, removed, entries }.
Concurrent Reads
const [config, selectedTask] = await Promise.all([
window.Magic.fs.readFile("data/config.json").then(JSON.parse),
window.Magic.fs.readFile(selectedEntry.path).then(JSON.parse),
]);
Shared Data Storage Rules
For generated CRUD micro-apps, assume multiple users may share the same app.
- Config or single current state may use one overwritable file, such as
data/config.json.
- User-created business records must default to one file per record, such as
data/tasks/<record-file>.json.
- List pages must render from
listDir() entries and file-name projection; do not batch readFile() every record just to draw a list.
- Event logs and history should be append-only multi-file records, such as
data/events/<timestamp>__<id>.json.
- Reports, analysis output, and caches may be overwritten because they are derived artifacts.
- For more than 500 expected records, bucket by month or business status, such as
data/tasks/2026-06/ or data/tasks/open/, and use pagination or virtual scrolling.
Record file names are list projections only:
<sortKey>__<status>__<shortId>__<titleSlug>.json
Required helpers in generated apps:
buildRecordFileName(record)
parseRecordFileName(name)
slugifyTitle(title) — lowercase English letters, digits, hyphens only; return record when unsafe or not representable.
truncateUtf8Bytes(input, maxBytes)
File-name limits:
- Hard limit: 255 bytes.
- Generation target: 120 bytes including
.json.
titleSlug: max 40 bytes by default.
- Forbidden:
/, \, <, >, :, ", |, ?, *, control chars, .., leading/trailing spaces.
- Never put phone numbers, addresses, notes, detailed amounts, private fields, or long text in file names.
- Always include stable
shortId. Never use only the title.
- Sort lists by parsed
sortKey, not by backend return order.
Update safety:
- Create: generate stable
id/shortId, build the file name, then create the record file. If the target file exists in the same dir, regenerate shortId.
- Update non-projection fields: write JSON only.
- Update title/status/date projection fields: write JSON first, then
renameFile(), preserving shortId.
- Before rename, call
listDir() and block the rename if the target name already exists with a different shortId.
- If JSON and file-name projection disagree, list uses file name, detail uses JSON. Try a background rename repair only when it cannot overwrite another file.
- Complex filters across more than two detail fields, amount ranges, tag combinations, owners, or similar query needs require an index file or backend query capability.
1.5 getAppBasePath() → Promise<string>
const basePath = await window.Magic.getAppBasePath();
// "personal-finance/" or "" (workspace root)
fs.* paths → relative to app root by default: "data/file.json"; project-root paths use a leading slash such as "/shared/file.json".
@file mention file_path → prefix: basePath + "data/file.json"
.magic/ paths → use as-is (already workspace root)
2. LLM API (window.Magic.llm)
getModels() → Promise<Model[]>
const models = await window.Magic.llm.getModels();
// [{id, object?, owned_by?, icon?, label?, info?}]
⚠️ model field required — default "auto". Empty string forbidden. Model selector UI must have "Auto Select" as first/default item.
chat(messages, options?) → Promise<string>
const reply = await window.Magic.llm.chat(
[{ role: "user", content: "How many planets?" }],
{ model: "auto" },
);
Options: model (required), temperature? (0-2), maxTokens?, systemPrompt?. Timeout: 120s.
stream(messages, onChunk, options?) → () => void
let text = "";
const cancel = window.Magic.llm.stream(
[{ role: "user", content: "Write about AI." }],
(delta, done) => {
text += delta;
if (done) console.log("Done");
},
{ model: "auto", maxTokens: 1000 },
);
onChunk: (delta: string, done: boolean) => void. Returns cancel fn.
chat and stream require llm.use in app.json.permissions.scopes.
3. Agent Interaction
setInputMessage(msg) → void
window.Magic.setInputMessage("Analysis complete. Please generate charts.");
reload() → void
window.Magic.reload();
4. Agent Namespace (window.Magic.agent)
getAgents() → Promise<AgentInfo[]>
const agents = await window.Magic.agent.getAgents();
// [{id, name, icon, color, type: "official"|"custom"|"public"}]
5. Project Namespace (window.Magic.project)
5.1 uploadFiles(files) → Promise<unknown>
Prefer fs.writeFile(path, blob) for single files.
await window.Magic.project.uploadFiles(
files.map((f) => ({ file: f, path: `./${f.name}`, filename: f.name })),
);
Max 500 MB per file.
Requires project.files.upload in app.json.permissions.scopes.
5.2 downloadFiles(paths) → Promise<unknown>
await window.Magic.project.downloadFiles(["output/report.pdf"]);
Requires project.files.download in app.json.permissions.scopes.
5.3 addFilesToMessage(filePaths, agentMode?) → Promise<unknown>
await window.Magic.project.addFilesToMessage(["data/report.csv"]);
Requires project.message.write in app.json.permissions.scopes.
5.4 createTopicAndSend(message, options?) → Promise<{topicId}>
Creates new topic. message: plain text or tiptap JSON (see tiptap ref).
// Plain text
const { topicId } = await window.Magic.project.createTopicAndSend(
"Analyze this",
{ model: "auto" },
);
// Tiptap JSON with @file mention (trigger companion skill)
const { topicId: t2 } = await window.Magic.project.createTopicAndSend(
{
type: "doc",
content: [
{
type: "paragraph",
content: [
{ type: "text", text: "Read the skill file and execute it: " },
{
type: "mention",
attrs: {
type: "project_file",
data: {
file_id: "skill_ref",
file_name: "SKILL.md",
file_path: ".magic/report_writer/SKILL.md",
file_extension: "md",
},
},
},
{ type: "text", text: "\n\nTask: generate a report" },
],
},
],
},
{ model: "auto" },
);
Options: agentId? (defaults general mode), model? (default "auto"). Timeout: 30s.
Requires project.message.write in app.json.permissions.scopes.
5.5 sendMessage(message, options?) → Promise<void>
await window.Magic.project.sendMessage("Continue analyzing", { model: "auto" });
Options: model?. Timeout: 15s.
Requires project.message.write in app.json.permissions.scopes.
6. User Info (window.Magic.user)
getInfo(options?) → Promise<UserInfo>
Default call returns only display-safe fields:
const user = await window.Magic.user.getInfo();
// {name, avatar}
document.getElementById("avatar").src = user.avatar;
Sensitive fields require permission declaration in app.json in the same folder as index.html. app.json is the declarative manifest read by the host before authorization checks; do not declare user info scopes in magic.project.js.
{
"name": "Profile Card",
"permissions": {
"scopes": ["user.profile.name", "user.profile.identity"],
"reason": "Display the current user's profile"
}
}
Then request the declared scopes at runtime:
try {
const user = await window.Magic.user.getInfo({
scopes: ["user.profile.name", "user.profile.identity"],
reason: "Display the current user's profile",
});
// {name, avatar, nickname, real_name, user_id, magic_id}
} catch (err) {
// Rejected when scopes are undeclared or the user denies authorization.
}
| Scope |
Returned fields |
Authorization |
user.profile.display |
name, avatar |
No prompt; default |
user.profile.name |
nickname, real_name |
Requires declaration and user confirmation |
user.profile.identity |
user_id, magic_id |
Requires declaration and user confirmation |
user.profile.organization |
organization_code |
Requires declaration and user confirmation |
| Field |
Type |
Description |
name |
string |
Display name (real_name > nickname) |
avatar |
string |
Avatar URL |
nickname |
string |
Nickname; only with user.profile.name |
real_name |
string |
Real name; only with user.profile.name |
user_id |
string |
User ID in current org; only with user.profile.identity |
magic_id |
string |
Global unique ID; only with user.profile.identity |
organization_code |
string |
Current org code; only with user.profile.organization |
Notes:
- Sensitive scopes must be present in both
app.json.permissions.scopes and the runtime getInfo({ scopes }) call.
magic.project.js is legacy for older HTML micro-apps and still used by other project types such as slides/design/media. It is not the HTML micro-app manifest.
reason should explain why the app needs these fields; runtime reason overrides the app.json reason in the confirmation dialog.
- Approved sensitive scopes use the same host authorization store as other high-risk APIs. They remain valid only in the current browser tab for the duration selected by the user, and the user can revoke them from the HTML app authorization manager.
- Never assume identity or organization fields are available from a bare
getInfo() call.
Timeout: 15s.
6.5 Permission Declaration
New HTML micro-apps must declare every high-risk scope they may request:
{
"version": "1.0.0",
"type": "micro-app",
"name": "Report Assistant",
"entry": "index.html",
"permissions": {
"scopes": [
"llm.use",
"fs.project.read",
"fs.project.write",
"project.files.download",
"project.message.write"
],
"reason": "Read selected project files, call AI, and write generated reports back to the project"
}
}
High-risk scopes:
| Scope |
Required for |
llm.use |
window.Magic.llm.chat, window.Magic.llm.stream |
fs.project.read |
Project-root fs.readFile("/..."), fs.getFileUrl("/...") |
fs.project.write |
Project-root fs.writeFile, deleteFile, deleteDir, moveFile, renameFile |
project.files.upload |
window.Magic.project.uploadFiles |
project.files.download |
window.Magic.project.downloadFiles |
project.message.write |
addFilesToMessage, createTopicAndSend, sendMessage |
user.profile.name |
user.getInfo({ scopes: ["user.profile.name"] }) |
user.profile.identity |
user.getInfo({ scopes: ["user.profile.identity"] }) |
user.profile.organization |
user.getInfo({ scopes: ["user.profile.organization"] }) |
Historical apps without app.json can still request high-risk APIs, but the host treats them as legacy apps: the user must approve the request, the approval duration is shorter, and the dialog warns that the app has no permission declaration. New apps should not rely on legacy behavior.
7. Backward Compatibility
| Deprecated |
New Path |
window.Magic.getAgents() |
window.Magic.agent.getAgents() |
window.Magic.uploadFiles(files) |
window.Magic.project.uploadFiles(files) |
window.Magic.downloadFiles(paths) |
window.Magic.project.downloadFiles(paths) |
window.Magic.addFilesToMessage(files) |
window.Magic.project.addFilesToMessage(files) |
window.Magic.createTopicAndSend(msg, opts?) |
window.Magic.project.createTopicAndSend(msg, opts?) |
window.Magic.sendMessage(msg, opts?) |
window.Magic.project.sendMessage(msg, opts?) |
8. Error Handling
// fs: file not found
try {
return JSON.parse(await window.Magic.fs.readFile("data/config.json"));
} catch (err) {
if (err.message.includes("not found")) return { theme: "light" };
throw err;
}
// llm: timeout
try {
return await window.Magic.llm.chat(messages, { model: "auto" });
} catch (err) {
if (err.message.includes("timed out")) return "Request timed out.";
return "Failed: " + err.message;
}
// stream: done=true signals end (including errors)
window.Magic.llm.stream(
messages,
(delta, done) => {
buffer += delta;
if (done) finalize(buffer);
},
{ model: "auto" },
);
9. API Quick Reference
| API |
Returns |
window.Magic.getAppBasePath() |
Promise<string> |
window.Magic.fs.readFile(path) |
Promise<string> |
window.Magic.fs.writeFile(path, content) |
Promise<void> |
window.Magic.fs.listFiles(dir?) |
Promise<string[]> |
window.Magic.fs.listDir(dir?) |
Promise<DirEntry[]> |
window.Magic.fs.getFileUrl(path) |
Promise<string> |
window.Magic.fs.deleteFile(path) |
Promise<void> |
window.Magic.fs.deleteDir(path) |
Promise<void> |
window.Magic.fs.moveFile(path, targetDir) |
Promise<void> |
window.Magic.fs.renameFile(path, newName) |
Promise<void> |
window.Magic.fs.watchFile(path, cb) |
() => void |
window.Magic.fs.watchDir(dir, cb) |
() => void |
window.Magic.llm.getModels() |
Promise<Model[]> |
window.Magic.llm.chat(msgs, opts?) |
Promise<string> |
window.Magic.llm.stream(msgs, onChunk, opts?) |
() => void |
window.Magic.setInputMessage(msg) |
void |
window.Magic.reload() |
void |
window.Magic.agent.getAgents() |
Promise<AgentInfo[]> |
window.Magic.project.uploadFiles(files) |
Promise<unknown> |
window.Magic.project.downloadFiles(paths) |
Promise<unknown> |
window.Magic.project.addFilesToMessage(files) |
Promise<unknown> |
window.Magic.project.createTopicAndSend(msg, opts?) |
Promise<{topicId}> |
window.Magic.project.sendMessage(msg, opts?) |
Promise<void> |
window.Magic.user.getInfo(options?) |
Promise<UserInfo> |
1---2name: html-api-sdk3description: Complete API reference for window.Magic.* in SuperMagic HTML micro-apps (HTML 微应用). Read this skill when you need exact method signatures, parameters, return types, or usage examples for: fs (readFile/writeFile/listFiles/listDir/getFileUrl/deleteFile/deleteDir/moveFile/renameFile/watchFile/watchDir), llm (chat/stream/getModels), agent (getAgents/selectAgent), project (createTopicAndSend/sendMessage/uploadFiles/downloadFiles), user (getInfo with app.json userInfo scopes), getAppBasePath, setInputMessage, reload. Also covers file-per-record data storage, list projection file names, tiptap JSON message format, @file and @skill mention structures, model selector UI rules, user info authorization, error handling patterns, and backward compatibility table. Trigger phrases: 'window.Magic API', 'readFile writeFile', 'listDir watchDir', 'getFileUrl', 'get file url', '文件 URL', '获取文件链接', 'deleteFile deleteDir', 'moveFile renameFile', 'watchFile callback', 'watchDir callback', 'llm.stream', 'llm.chat', 'createTopicAndSen4---5
6# window.Magic API — HTML Micro-App Guide
7
8## How to Use This Document
9
10- API signatures & constraints → this document
11- App manifest & permission declarations → `app.json`
12- TiptapJSON & @mention structures → [references/tiptap-json-format.md](references/tiptap-json-format.md)
13- Complete HTML examples → [references/complete-examples.md](references/complete-examples.md)
14
15## Important Constraints
16
171. All `window.Magic.*` APIs are **pre-injected** — no imports needed. External CDN allowed.
182. File paths are relative to **app root** (`index.html` dir) by default. `../` is forbidden. Use leading-slash paths such as `"/shared/data.json"` to access project-root files. Writing, deleting, moving, or renaming files outside the app root triggers host confirmation.
193. `window.Magic.llm` tokens hosted; no `api_key` in HTML.
204. **No inline event handlers** — use `addEventListener`. For buttons rendered by `innerHTML`, bind one delegated listener on a stable container and use `data-action`/`data-id`.
215. **LLM calls must include model selector UI** unless user specifies model. Default `"auto"`.
226. **Complex file-based AI** → use `createTopicAndSend` + `@file` + companion skill. Simple → `readFile` + `llm.chat/stream`.
237. **High-risk APIs are permission-gated** — new HTML micro-apps must declare requested scopes in `app.json.permissions.scopes`. The host asks the user to approve high-risk runtime calls for a limited duration.
248. **User info is privacy-gated** — `window.Magic.user.getInfo()` returns only `name` and `avatar` by default. Sensitive fields require a matching permission declaration, a runtime `getInfo({ scopes, reason })` request, and user confirmation.
259. **Use `app.json` as the micro-app manifest** — every new HTML micro-app folder should include `app.json` next to `index.html`. Put `type`, `name`, `entry`, `anonymous`, file aliases, watch hints, and permissions there. Also generate a minimal `magic.project.js` display bridge that mirrors only `version/type/name/entry/icon`; do not put `anonymous`, permissions, files, watch, or business state in `magic.project.js`.
26 ```json
27 {
28 "version": "1.0.0",
29 "type": "micro-app",
30 "name": "App Name",
31 "entry": "index.html",
32 "anonymous": false,
33 "files": {},
34 "watch": [],
35 "permissions": {
36 "scopes": [],
37 "reason": ""
38 }
39 }
40 ```
41
4210. **Administrator page access is runtime-controlled** — when an app has administrator-only pages, put `window.MagicAppConfig.admin_pages` in the shared `app.js` and call `window.Magic.db.getProjectAdminAccess()` before loading each listed page. The result is based on the real logged-in user; a share token is only an access proof and is never a user identity.
43
44---
45
46## HTML Interaction Safety
47
48Generated micro-app controls must be wired through real JavaScript listeners, not HTML event attributes.
49
50- Do not generate `onclick`, `onchange`, `oninput`, `onsubmit`, or other inline event attributes.
51- For lists, cards, table rows, and menus rendered with `innerHTML`, use event delegation: `container.addEventListener("click", handler)` and buttons such as `<button data-action="edit" data-id="...">`.
52- Do not attach action functions to `window` just to make inline event handlers work.
53- If using `new FormData(form)`, every value read with `formData.get("field")` must have a matching `name="field"` on the input/select/textarea. Having only `id="field"` is not enough.
54- Before calling `.trim()`, normalize possibly missing form values, for example `String(formData.get("title") || "").trim()`.
55- If a form is read by DOM IDs instead, use `.value` consistently and do not mix it with `FormData.get()` for unnamed controls.
56
57## 1. File System (`window.Magic.fs`)
58
59### `readFile(path)` → `Promise<string>`
60
61```javascript
62const raw = await window.Magic.fs.readFile("data/tasks/20260624153000__open__a8f3k2__follow-up.json");
63const task = JSON.parse(raw);
64```
65
66- `path: string` — relative to app root. Max 5 MB; rejects if not found.
67
68### `writeFile(path, content)` → `Promise<void>`
69
70```javascript
71await window.Magic.fs.writeFile(
72 "data/tasks/20260624153000__open__a8f3k2__follow-up.json",
73 JSON.stringify(record, null, 2),
74);
75// Binary (up to 500 MB):
76await window.Magic.fs.writeFile("data/large.bin", blob);
77```
78
79- `content: string | Blob | ArrayBuffer`. String max 5 MB. Auto-creates dirs. `../` blocked.
80
81> ⚠️ Paths relative to `index.html` dir, NOT workspace root.
82
83### File Paths and Project-Root Access
84
85By default, relative `window.Magic.fs.*` paths resolve inside the app folder next to `index.html`. Use a leading slash for project-root paths. Project-root reads require `fs.project.read`; project-root writes/deletes/moves/renames require `fs.project.write` plus a host path confirmation for each destructive operation.
86
87Path rules:
88
89- `"data/config.json"` -> app root, e.g. `my-app/data/config.json`.
90- `"/shared/config.json"` -> project root.
91- `"/"` lists project-root entries.
92- `../` remains blocked in all scopes.
93- Reading project-root file contents or temporary URLs requires `fs.project.read`.
94- Writing, deleting, moving, or renaming files outside the app root requires `fs.project.write`, then triggers host path confirmation and may be rejected by the user.
95- `listFiles("/")` and `listDir("/")` are not gated in the current version, but do not depend on them for sensitive directory discovery.
96
97### `listFiles(dir?)` → `Promise<string[]>`
98
99```javascript
100const files = await window.Magic.fs.listFiles("data/");
101```
102
103- Compatibility API. It returns direct child names only. Prefer `listDir()` for new list UIs.
104
105### `listDir(dir?)` → `Promise<Array<{name,path,isDirectory,updatedAt?}>>`
106
107```javascript
108const entries = await window.Magic.fs.listDir("data/tasks/");
109entries
110 .map((entry) => parseRecordFileName(entry.name))
111 .filter(Boolean)
112 .sort((a, b) => b.sortKey.localeCompare(a.sortKey));
113```
114
115- Returns direct children only. It does not read file contents.
116- Use it for list pages. Read the JSON detail only when the user opens, edits, or analyzes one record.
117- `path` is usable with `readFile`, `writeFile`, `deleteFile`, `moveFile`, and `renameFile`.
118
119### `getFileUrl(path)` → `Promise<string>`
120
121```javascript
122const imageUrl = await window.Magic.fs.getFileUrl("assets/chart.png");
123document.getElementById("preview").src = imageUrl;
124```
125
126- Returns a temporary browser-accessible URL for an existing workspace file.
127- Use it for previews, `<img>`, `<audio>`, `<video>`, download links, or libraries that need a URL instead of file text.
128- It does not download the file by itself. Use `window.Magic.project.downloadFiles(paths)` when the user action should trigger a browser download.
129- Rejects if the file is missing or the path is invalid. `../` blocked.
130
131### `deleteFile(path)` → `Promise<void>`
132
133```javascript
134await window.Magic.fs.deleteFile("data/temp.json");
135```
136
137- Rejects if file not found. `../` blocked.
138
139### `deleteDir(path)` → `Promise<void>`
140
141```javascript
142await window.Magic.fs.deleteDir("temp/");
143```
144
145- Recursively deletes all files and subdirectories. Cannot delete app root or project root. Rejects if dir not found. `../` blocked.
146
147### `moveFile(path, targetDir)` → `Promise<void>`
148
149```javascript
150await window.Magic.fs.moveFile("data/old.json", "archive/");
151```
152
153- Moves a file or directory to the specified target parent directory. Rejects if source file or target directory not found. `../` blocked.
154
155### `renameFile(path, newName)` → `Promise<void>`
156
157```javascript
158await window.Magic.fs.renameFile("data/draft.txt", "final.txt");
159```
160
161- Renames a file or directory. `newName` is just the new name (no path separators). Rejects if file not found. `../` blocked.
162
163### `watchFile(path, cb)` → `() => void`
164
165```javascript
166const unwatch = window.Magic.fs.watchFile("data/orders.json", async (e) => {
167 const fresh = JSON.parse(await window.Magic.fs.readFile("data/orders.json"));
168 renderTable(fresh);
169});
170```
171
172- Polls ~3s; max 10 watched paths per app. Call returned fn to stop.
173
174### `watchDir(dir, cb)` → `() => void`
175
176```javascript
177const unwatch = window.Magic.fs.watchDir("data/tasks/", (event) => {
178 // renameFile that changes projection appears as removed + added.
179 // Use parseRecordFileName(name).shortId to match the same record.
180 renderList(event.entries);
181});
182```
183
184- Not a real-time filesystem watcher. It compares refreshed host attachment snapshots after the existing attachment polling or `Update_Attachments` refresh.
185- Watches direct child additions and removals only. File content changes continue to use `watchFile()`.
186- Callback payload: `{ dir, timestamp, added, removed, entries }`.
187
188### Concurrent Reads
189
190```javascript
191const [config, selectedTask] = await Promise.all([
192 window.Magic.fs.readFile("data/config.json").then(JSON.parse),
193 window.Magic.fs.readFile(selectedEntry.path).then(JSON.parse),
194]);
195```
196
197### Shared Data Storage Rules
198
199For generated CRUD micro-apps, assume multiple users may share the same app.
200
201- Config or single current state may use one overwritable file, such as `data/config.json`.
202- User-created business records must default to one file per record, such as `data/tasks/<record-file>.json`.
203- List pages must render from `listDir()` entries and file-name projection; do not batch `readFile()` every record just to draw a list.
204- Event logs and history should be append-only multi-file records, such as `data/events/<timestamp>__<id>.json`.
205- Reports, analysis output, and caches may be overwritten because they are derived artifacts.
206- For more than 500 expected records, bucket by month or business status, such as `data/tasks/2026-06/` or `data/tasks/open/`, and use pagination or virtual scrolling.
207
208Record file names are list projections only:
209
210```text
211<sortKey>__<status>__<shortId>__<titleSlug>.json
212```
213
214Required helpers in generated apps:
215
216- `buildRecordFileName(record)`
217- `parseRecordFileName(name)`
218- `slugifyTitle(title)` — lowercase English letters, digits, hyphens only; return `record` when unsafe or not representable.
219- `truncateUtf8Bytes(input, maxBytes)`
220
221File-name limits:
222
223- Hard limit: 255 bytes.
224- Generation target: 120 bytes including `.json`.
225- `titleSlug`: max 40 bytes by default.
226- Forbidden: `/`, `\`, `<`, `>`, `:`, `"`, `|`, `?`, `*`, control chars, `..`, leading/trailing spaces.
227- Never put phone numbers, addresses, notes, detailed amounts, private fields, or long text in file names.
228- Always include stable `shortId`. Never use only the title.
229- Sort lists by parsed `sortKey`, not by backend return order.
230
231Update safety:
232
233- Create: generate stable `id/shortId`, build the file name, then create the record file. If the target file exists in the same dir, regenerate `shortId`.
234- Update non-projection fields: write JSON only.
235- Update title/status/date projection fields: write JSON first, then `renameFile()`, preserving `shortId`.
236- Before rename, call `listDir()` and block the rename if the target name already exists with a different `shortId`.
237- If JSON and file-name projection disagree, list uses file name, detail uses JSON. Try a background rename repair only when it cannot overwrite another file.
238- Complex filters across more than two detail fields, amount ranges, tag combinations, owners, or similar query needs require an index file or backend query capability.
239
240---
241
242## 1.5 `getAppBasePath()` → `Promise<string>`
243
244```javascript
245const basePath = await window.Magic.getAppBasePath();
246// "personal-finance/" or "" (workspace root)
247```
248
249- `fs.*` paths → relative to app root by default: `"data/file.json"`; project-root paths use a leading slash such as `"/shared/file.json"`.
250- `@file` mention `file_path` → prefix: `basePath + "data/file.json"`
251- `.magic/` paths → use as-is (already workspace root)
252
253---
254
255## 2. LLM API (`window.Magic.llm`)
256
257### `getModels()` → `Promise<Model[]>`
258
259```javascript
260const models = await window.Magic.llm.getModels();
261// [{id, object?, owned_by?, icon?, label?, info?}]
262```
263
264> ⚠️ `model` field **required** — default `"auto"`. Empty string forbidden. Model selector UI must have "Auto Select" as first/default item.
265
266### `chat(messages, options?)` → `Promise<string>`
267
268```javascript
269const reply = await window.Magic.llm.chat(
270 [{ role: "user", content: "How many planets?" }],
271 { model: "auto" },
272);
273```
274
275Options: `model` (required), `temperature?` (0-2), `maxTokens?`, `systemPrompt?`. Timeout: 120s.
276
277### `stream(messages, onChunk, options?)` → `() => void`
278
279```javascript
280let text = "";
281const cancel = window.Magic.llm.stream(
282 [{ role: "user", content: "Write about AI." }],
283 (delta, done) => {
284 text += delta;
285 if (done) console.log("Done");
286 },
287 { model: "auto", maxTokens: 1000 },
288);
289```
290
291`onChunk: (delta: string, done: boolean) => void`. Returns cancel fn.
292
293`chat` and `stream` require `llm.use` in `app.json.permissions.scopes`.
294
295---
296
297## 3. Agent Interaction
298
299### `setInputMessage(msg)` → `void`
300
301```javascript
302window.Magic.setInputMessage("Analysis complete. Please generate charts.");
303```
304
305### `reload()` → `void`
306
307```javascript
308window.Magic.reload();
309```
310
311---
312
313## 4. Agent Namespace (`window.Magic.agent`)
314
315### `getAgents()` → `Promise<AgentInfo[]>`
316
317```javascript
318const agents = await window.Magic.agent.getAgents();
319// [{id, name, icon, color, type: "official"|"custom"|"public"}]
320```
321
322---
323
324## 5. Project Namespace (`window.Magic.project`)
325
326### 5.1 `uploadFiles(files)` → `Promise<unknown>`
327
328> Prefer `fs.writeFile(path, blob)` for single files.
329
330```javascript
331await window.Magic.project.uploadFiles(
332 files.map((f) => ({ file: f, path: `./${f.name}`, filename: f.name })),
333);
334```
335
336Max 500 MB per file.
337
338Requires `project.files.upload` in `app.json.permissions.scopes`.
339
340### 5.2 `downloadFiles(paths)` → `Promise<unknown>`
341
342```javascript
343await window.Magic.project.downloadFiles(["output/report.pdf"]);
344```
345
346Requires `project.files.download` in `app.json.permissions.scopes`.
347
348### 5.3 `addFilesToMessage(filePaths, agentMode?)` → `Promise<unknown>`
349
350```javascript
351await window.Magic.project.addFilesToMessage(["data/report.csv"]);
352```
353
354Requires `project.message.write` in `app.json.permissions.scopes`.
355
356### 5.4 `createTopicAndSend(message, options?)` → `Promise<{topicId}>`
357
358Creates new topic. `message`: plain text or tiptap JSON (see [tiptap ref](references/tiptap-json-format.md)).
359
360```javascript
361// Plain text
362const { topicId } = await window.Magic.project.createTopicAndSend(
363 "Analyze this",
364 { model: "auto" },
365);
366
367// Tiptap JSON with @file mention (trigger companion skill)
368const { topicId: t2 } = await window.Magic.project.createTopicAndSend(
369 {
370 type: "doc",
371 content: [
372 {
373 type: "paragraph",
374 content: [
375 { type: "text", text: "Read the skill file and execute it: " },
376 {
377 type: "mention",
378 attrs: {
379 type: "project_file",
380 data: {
381 file_id: "skill_ref",
382 file_name: "SKILL.md",
383 file_path: ".magic/report_writer/SKILL.md",
384 file_extension: "md",
385 },
386 },
387 },
388 { type: "text", text: "\n\nTask: generate a report" },
389 ],
390 },
391 ],
392 },
393 { model: "auto" },
394);
395```
396
397Options: `agentId?` (defaults general mode), `model?` (default `"auto"`). Timeout: 30s.
398
399Requires `project.message.write` in `app.json.permissions.scopes`.
400
401### 5.5 `sendMessage(message, options?)` → `Promise<void>`
402
403```javascript
404await window.Magic.project.sendMessage("Continue analyzing", { model: "auto" });
405```
406
407Options: `model?`. Timeout: 15s.
408
409Requires `project.message.write` in `app.json.permissions.scopes`.
410
411---
412
413## 6. User Info (`window.Magic.user`)
414
415### `getInfo(options?)` → `Promise<UserInfo>`
416
417Default call returns only display-safe fields:
418
419```javascript
420const user = await window.Magic.user.getInfo();
421// {name, avatar}
422document.getElementById("avatar").src = user.avatar;
423```
424
425Sensitive fields require permission declaration in `app.json` in the same folder as `index.html`. `app.json` is the declarative manifest read by the host before authorization checks; do not declare user info scopes in `magic.project.js`.
426
427```json
428{
429 "name": "Profile Card",
430 "permissions": {
431 "scopes": ["user.profile.name", "user.profile.identity"],
432 "reason": "Display the current user's profile"
433 }
434}
435```
436
437Then request the declared scopes at runtime:
438
439```javascript
440try {
441 const user = await window.Magic.user.getInfo({
442 scopes: ["user.profile.name", "user.profile.identity"],
443 reason: "Display the current user's profile",
444 });
445 // {name, avatar, nickname, real_name, user_id, magic_id}
446} catch (err) {
447 // Rejected when scopes are undeclared or the user denies authorization.
448}
449```
450
451| Scope | Returned fields | Authorization |
452| --- | --- | --- |
453| `user.profile.display` | `name`, `avatar` | No prompt; default |
454| `user.profile.name` | `nickname`, `real_name` | Requires declaration and user confirmation |
455| `user.profile.identity` | `user_id`, `magic_id` | Requires declaration and user confirmation |
456| `user.profile.organization` | `organization_code` | Requires declaration and user confirmation |
457
458| Field | Type | Description |
459| --- | --- | --- |
460| `name` | `string` | Display name (real_name > nickname) |
461| `avatar` | `string` | Avatar URL |
462| `nickname` | `string` | Nickname; only with `user.profile.name` |
463| `real_name` | `string` | Real name; only with `user.profile.name` |
464| `user_id` | `string` | User ID in current org; only with `user.profile.identity` |
465| `magic_id` | `string` | Global unique ID; only with `user.profile.identity` |
466| `organization_code` | `string` | Current org code; only with `user.profile.organization` |
467
468Notes:
469
470- Sensitive scopes must be present in both `app.json.permissions.scopes` and the runtime `getInfo({ scopes })` call.
471- `magic.project.js` is legacy for older HTML micro-apps and still used by other project types such as slides/design/media. It is not the HTML micro-app manifest.
472- `reason` should explain why the app needs these fields; runtime `reason` overrides the `app.json` reason in the confirmation dialog.
473- Approved sensitive scopes use the same host authorization store as other high-risk APIs. They remain valid only in the current browser tab for the duration selected by the user, and the user can revoke them from the HTML app authorization manager.
474- Never assume identity or organization fields are available from a bare `getInfo()` call.
475
476Timeout: 15s.
477
478---
479
480## 6.5 Permission Declaration
481
482New HTML micro-apps must declare every high-risk scope they may request:
483
484```json
485{
486 "version": "1.0.0",
487 "type": "micro-app",
488 "name": "Report Assistant",
489 "entry": "index.html",
490 "permissions": {
491 "scopes": [
492 "llm.use",
493 "fs.project.read",
494 "fs.project.write",
495 "project.files.download",
496 "project.message.write"
497 ],
498 "reason": "Read selected project files, call AI, and write generated reports back to the project"
499 }
500}
501```
502
503High-risk scopes:
504
505| Scope | Required for |
506| --- | --- |
507| `llm.use` | `window.Magic.llm.chat`, `window.Magic.llm.stream` |
508| `fs.project.read` | Project-root `fs.readFile("/...")`, `fs.getFileUrl("/...")` |
509| `fs.project.write` | Project-root `fs.writeFile`, `deleteFile`, `deleteDir`, `moveFile`, `renameFile` |
510| `project.files.upload` | `window.Magic.project.uploadFiles` |
511| `project.files.download` | `window.Magic.project.downloadFiles` |
512| `project.message.write` | `addFilesToMessage`, `createTopicAndSend`, `sendMessage` |
513| `user.profile.name` | `user.getInfo({ scopes: ["user.profile.name"] })` |
514| `user.profile.identity` | `user.getInfo({ scopes: ["user.profile.identity"] })` |
515| `user.profile.organization` | `user.getInfo({ scopes: ["user.profile.organization"] })` |
516
517Historical apps without `app.json` can still request high-risk APIs, but the host treats them as legacy apps: the user must approve the request, the approval duration is shorter, and the dialog warns that the app has no permission declaration. New apps should not rely on legacy behavior.
518
519---
520
521## 7. Backward Compatibility
522
523| Deprecated | New Path |
524| --- | --- |
525| `window.Magic.getAgents()` | `window.Magic.agent.getAgents()` |
526| `window.Magic.uploadFiles(files)` | `window.Magic.project.uploadFiles(files)` |
527| `window.Magic.downloadFiles(paths)` | `window.Magic.project.downloadFiles(paths)` |
528| `window.Magic.addFilesToMessage(files)` | `window.Magic.project.addFilesToMessage(files)` |
529| `window.Magic.createTopicAndSend(msg, opts?)` | `window.Magic.project.createTopicAndSend(msg, opts?)` |
530| `window.Magic.sendMessage(msg, opts?)` | `window.Magic.project.sendMessage(msg, opts?)` |
531
532---
533
534## 8. Error Handling
535
536```javascript
537// fs: file not found
538try {
539 return JSON.parse(await window.Magic.fs.readFile("data/config.json"));
540} catch (err) {
541 if (err.message.includes("not found")) return { theme: "light" };
542 throw err;
543}
544
545// llm: timeout
546try {
547 return await window.Magic.llm.chat(messages, { model: "auto" });
548} catch (err) {
549 if (err.message.includes("timed out")) return "Request timed out.";
550 return "Failed: " + err.message;
551}
552
553// stream: done=true signals end (including errors)
554window.Magic.llm.stream(
555 messages,
556 (delta, done) => {
557 buffer += delta;
558 if (done) finalize(buffer);
559 },
560 { model: "auto" },
561);
562```
563
564---
565
566## 9. API Quick Reference
567
568| API | Returns |
569| --- | --- |
570| `window.Magic.getAppBasePath()` | `Promise<string>` |
571| `window.Magic.fs.readFile(path)` | `Promise<string>` |
572| `window.Magic.fs.writeFile(path, content)` | `Promise<void>` |
573| `window.Magic.fs.listFiles(dir?)` | `Promise<string[]>` |
574| `window.Magic.fs.listDir(dir?)` | `Promise<DirEntry[]>` |
575| `window.Magic.fs.getFileUrl(path)` | `Promise<string>` |
576| `window.Magic.fs.deleteFile(path)` | `Promise<void>` |
577| `window.Magic.fs.deleteDir(path)` | `Promise<void>` |
578| `window.Magic.fs.moveFile(path, targetDir)` | `Promise<void>` |
579| `window.Magic.fs.renameFile(path, newName)` | `Promise<void>` |
580| `window.Magic.fs.watchFile(path, cb)` | `() => void` |
581| `window.Magic.fs.watchDir(dir, cb)` | `() => void` |
582| `window.Magic.llm.getModels()` | `Promise<Model[]>` |
583| `window.Magic.llm.chat(msgs, opts?)` | `Promise<string>` |
584| `window.Magic.llm.stream(msgs, onChunk, opts?)` | `() => void` |
585| `window.Magic.setInputMessage(msg)` | `void` |
586| `window.Magic.reload()` | `void` |
587| `window.Magic.agent.getAgents()` | `Promise<AgentInfo[]>` |
588| `window.Magic.project.uploadFiles(files)` | `Promise<unknown>` |
589| `window.Magic.project.downloadFiles(paths)` | `Promise<unknown>` |
590| `window.Magic.project.addFilesToMessage(files)` | `Promise<unknown>` |
591| `window.Magic.project.createTopicAndSend(msg, opts?)` | `Promise<{topicId}>` |
592| `window.Magic.project.sendMessage(msg, opts?)` | `Promise<void>` |
593| `window.Magic.user.getInfo(options?)` | `Promise<UserInfo>` |