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', 'createTopicAndSend format', 'tiptap JSON mention', '@file mention structure', '@skill mention', 'getAppBasePath usage', 'model selector UI', 'user.getInfo', 'get user info', 'user avatar', 'userInfo scopes', 'app.json permissions', 'Magic API 用法', 'fs 读写文件 API', 'fs 获取文件链接', 'fs 删除文件', 'fs 移动重命名', '目录监听', '文件监听回调', '话题消息格式', 'mention 结构', '模型选择器', '用户信息', '用户授权', '获取头像'.
app.jsonAll 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 and Agent calls must include model selector UI unless the user specifies a fixed model. Load the available models with window.Magic.llm.getModels(), render them in the page, and preselect the first available model. Do not guide users to use "auto" as the default selection.
Complex file-based AI → use createTopicAndSend with the task; published micro-app previews automatically provide @micro_app context so the Agent can inspect companion skills. Use an explicit @file only when selecting one exact 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 version/name/entry/icon; new bridges use type: "html-app", while existing type: "micro-app" bridges remain compatible. 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.
Generated micro-app controls must be wired through real JavaScript listeners, not HTML event attributes.
onclick, onchange, oninput, onsubmit, or other inline event attributes.innerHTML, use event delegation: container.addEventListener("click", handler) and buttons such as <button data-action="edit" data-id="...">.window just to make inline event handlers work.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..trim(), normalize possibly missing form values, for example String(formData.get("title") || "").trim()..value consistently and do not mix it with FormData.get() for unnamed controls.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.htmldir, NOT workspace root.
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.fs.project.read.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/");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));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;<img>, <audio>, <video>, download links, or libraries that need a URL instead of file text.window.Magic.project.downloadFiles(paths) when the user action should trigger a browser download.../ blocked.deleteFile(path) → Promise<void>await window.Magic.fs.deleteFile("data/temp.json");../ blocked.deleteDir(path) → Promise<void>await window.Magic.fs.deleteDir("temp/");../ blocked.moveFile(path, targetDir) → Promise<void>await window.Magic.fs.moveFile("data/old.json", "archive/");../ blocked.renameFile(path, newName) → Promise<void>await window.Magic.fs.renameFile("data/draft.txt", "final.txt");newName is just the new name (no path separators). Rejects if file not found. ../ blocked.watchFile(path, cb) → () => voidconst unwatch = window.Magic.fs.watchFile("data/orders.json", async (e) => {
const fresh = JSON.parse(await window.Magic.fs.readFile("data/orders.json"));
renderTable(fresh);
});watchDir(dir, cb) → () => voidconst 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);
});Update_Attachments refresh.watchFile().{ dir, timestamp, added, removed, entries }.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),
]);For generated CRUD micro-apps, assume multiple users may share the same app.
data/config.json.data/tasks/<record-file>.json.listDir() entries and file-name projection; do not batch readFile() every record just to draw a list.data/events/<timestamp>__<id>.json.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>.jsonRequired 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:
.json.titleSlug: max 40 bytes by default./, \, <, >, :, ", |, ?, *, control chars, .., leading/trailing spaces.shortId. Never use only the title.sortKey, not by backend return order.Update safety:
id/shortId, build the file name, then create the record file. If the target file exists in the same dir, regenerate shortId.renameFile(), preserving shortId.listDir() and block the rename if the target name already exists with a different shortId.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)window.Magic.llm)getModels() → Promise<Model[]>const models = await window.Magic.llm.getModels();
// [{id, object?, owned_by?, icon?, label?, info?}]The model field is required for LLM calls. The page model selector should display the returned models and preselect the first available model. Handle an empty model list before sending a request; do not use "auto" as the default selector value.
chat(messages, options?) → Promise<string>const reply = await window.Magic.llm.chat(
[{ role: "user", content: "How many planets?" }],
// selectedModelId comes from the page model selector (default: first model)
{ model: selectedModelId },
);Options: model (required), temperature? (0-2), maxTokens?, systemPrompt?. Timeout: 120s.
stream(messages, onChunk, options?) → () => voidlet text = "";
const cancel = window.Magic.llm.stream(
[{ role: "user", content: "Write about AI." }],
(delta, done) => {
text += delta;
if (done) console.log("Done");
},
// selectedModelId comes from the page model selector (default: first model)
{ model: selectedModelId, maxTokens: 1000 },
);onChunk: (delta: string, done: boolean) => void. Returns cancel fn.
chat and stream require llm.use in app.json.permissions.scopes.
setInputMessage(msg) → voidwindow.Magic.setInputMessage("Analysis complete. Please generate charts.");reload() → voidwindow.Magic.reload();window.Magic.agent)getAgents() → Promise<AgentInfo[]>const agents = await window.Magic.agent.getAgents();
// [{id, name, icon, color, type: "official"|"custom"|"public"}]window.Magic.project)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.
downloadFiles(paths) → Promise<unknown>await window.Magic.project.downloadFiles(["output/report.pdf"]);Requires project.files.download in app.json.permissions.scopes.
addFilesToMessage(filePaths, agentMode?) → Promise<unknown>await window.Magic.project.addFilesToMessage(["data/report.csv"]);Requires project.message.write in app.json.permissions.scopes.
createTopicAndSend(message, options?) → Promise<{topicId}>Creates new topic. message: plain text or tiptap JSON (see tiptap ref).
When this API is called from a published micro-app preview, the host automatically prepends the current micro-app as a trusted @micro_app mention. The referenced project is mounted read-only. Name the companion skill in the task message, for example createTopicAndSend("Use the inventory_manager Skill to find low-stock products and generate replenishment suggestions.", { model: selectedModelId }), where selectedModelId is chosen by the page model selector and defaults to the first available model. The Agent loads that companion skill with read_skills(skill_names=["inventory_manager"], project_id=...) using the mention's project_id on the first attempt for that skill; the current runtime-project workspace is not the skill source. System skills omit project_id. When a task needs one exact skill file, the app may still include an explicit @file mention.
// Plain text
const { topicId } = await window.Magic.project.createTopicAndSend(
"Analyze this",
{ model: selectedModelId },
);
// 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/skills/report_writer/SKILL.md",
file_extension: "md",
},
},
},
{ type: "text", text: "\n\nTask: generate a report" },
],
},
],
},
{ model: selectedModelId },
);Options: agentId? (defaults general mode), model? (pass the model selected in the page model selector; default to the first available model). Timeout: 30s.
Requires project.message.write in app.json.permissions.scopes.
sendMessage(message, options?) → Promise<void>await window.Magic.project.sendMessage("Continue analyzing", { model: selectedModelId });selectedModelId should come from the page model selector, which displays getModels() results and defaults to the first available model. Timeout: 15s.
Requires project.message.write in app.json.permissions.scopes.
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:
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.getInfo() call.Timeout: 15s.
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.
| 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?) |
// 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: selectedModelId });
} 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: selectedModelId },
);| 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> |
dtyq/magic
0f1432c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.