@stage5/lumine 0.2.44 → 0.2.46
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/README.md +15 -0
- package/lib/api.js +57 -3
- package/lib/app-mcp/server.js +255 -0
- package/lib/commands.js +26 -4
- package/lib/constants.js +1 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +34 -9
- package/sdk/LUMINE_ADMIN.md +5 -3
package/README.md
CHANGED
|
@@ -113,6 +113,21 @@ Reference folders are marked `readOnly` in `.twinkle/lumine-project.json`.
|
|
|
113
113
|
Running `lumine save` from a reference folder is blocked; fork the source Build
|
|
114
114
|
first if you want an editable workspace.
|
|
115
115
|
|
|
116
|
+
## Using a published app over MCP
|
|
117
|
+
|
|
118
|
+
`lumine app-mcp <published-app-url-or-id>` turns an opted-in published Build
|
|
119
|
+
app into a standard stdio MCP server. Lumine pins the current published
|
|
120
|
+
artifact, reads its `/app-tools.json` manifest, and opens a dedicated signed-in
|
|
121
|
+
app tab. Tool calls run serially in that visible iframe through handlers the app
|
|
122
|
+
registered with `Twinkle.appTools.register({ handlers })`, so the agent and
|
|
123
|
+
viewer operate the same live UI and state. Keep that tab open while the MCP
|
|
124
|
+
client is connected. Pass `--no-open` only when you will open the URL printed on
|
|
125
|
+
stderr yourself.
|
|
126
|
+
|
|
127
|
+
Discovery is static and fail-closed: runtime code cannot add tools that were
|
|
128
|
+
not declared in the pinned manifest, and a session refuses to connect if any
|
|
129
|
+
declared handler is missing.
|
|
130
|
+
|
|
116
131
|
## Inspecting Build SDK data
|
|
117
132
|
|
|
118
133
|
`lumine sdk call <namespace.method> '<jsonArgs>'` calls a build's data SDK
|
package/lib/api.js
CHANGED
|
@@ -83,6 +83,63 @@ export async function loadBuildFiles({ options, auth, buildId, includeContent })
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export async function createAppMcpSession({ options, auth, buildId }) {
|
|
87
|
+
return await requestJson({
|
|
88
|
+
method: "POST",
|
|
89
|
+
url: `${options.apiUrl}/cli/build/${buildId}/app-mcp/sessions`,
|
|
90
|
+
authToken: auth.token,
|
|
91
|
+
body: {},
|
|
92
|
+
timeoutMs: options.timeoutMs,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function createAppMcpCall({
|
|
97
|
+
options,
|
|
98
|
+
auth,
|
|
99
|
+
buildId,
|
|
100
|
+
sessionId,
|
|
101
|
+
name,
|
|
102
|
+
arguments: toolArguments,
|
|
103
|
+
}) {
|
|
104
|
+
return await requestJson({
|
|
105
|
+
method: "POST",
|
|
106
|
+
url: `${options.apiUrl}/cli/build/${buildId}/app-mcp/sessions/${sessionId}/calls`,
|
|
107
|
+
authToken: auth.token,
|
|
108
|
+
body: { name, arguments: toolArguments ?? {} },
|
|
109
|
+
timeoutMs: options.timeoutMs,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function loadAppMcpCall({
|
|
114
|
+
options,
|
|
115
|
+
auth,
|
|
116
|
+
buildId,
|
|
117
|
+
sessionId,
|
|
118
|
+
callId,
|
|
119
|
+
}) {
|
|
120
|
+
return await requestJson({
|
|
121
|
+
method: "POST",
|
|
122
|
+
url: `${options.apiUrl}/cli/build/${buildId}/app-mcp/sessions/${sessionId}/calls/${callId}/status`,
|
|
123
|
+
authToken: auth.token,
|
|
124
|
+
body: {},
|
|
125
|
+
timeoutMs: options.timeoutMs,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function closeAppMcpSession({
|
|
130
|
+
options,
|
|
131
|
+
auth,
|
|
132
|
+
buildId,
|
|
133
|
+
sessionId,
|
|
134
|
+
}) {
|
|
135
|
+
return await requestJson({
|
|
136
|
+
method: "DELETE",
|
|
137
|
+
url: `${options.apiUrl}/cli/build/${buildId}/app-mcp/sessions/${sessionId}`,
|
|
138
|
+
authToken: auth.token,
|
|
139
|
+
timeoutMs: options.timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
86
143
|
export async function loadExternalAgentRuntime({
|
|
87
144
|
options,
|
|
88
145
|
auth,
|
|
@@ -412,9 +469,6 @@ export async function replaceMainWithContribution({
|
|
|
412
469
|
}
|
|
413
470
|
|
|
414
471
|
export async function publishBuild({ options, buildId, auth }) {
|
|
415
|
-
if (auth.releaseStatus?.state === "up_to_date") {
|
|
416
|
-
return { skipped: true };
|
|
417
|
-
}
|
|
418
472
|
try {
|
|
419
473
|
const result = await requestJson({
|
|
420
474
|
method: "POST",
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
closeAppMcpSession,
|
|
6
|
+
createAppMcpCall,
|
|
7
|
+
createAppMcpSession,
|
|
8
|
+
loadAppMcpCall,
|
|
9
|
+
} from "../api.js";
|
|
10
|
+
import { assertAuthScope, resolveAuth } from "../auth.js";
|
|
11
|
+
import { resolveRequiredBuildId } from "../util.js";
|
|
12
|
+
|
|
13
|
+
const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
14
|
+
const CALL_POLL_MS = 250;
|
|
15
|
+
const CALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
function delay(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function openApp(url) {
|
|
22
|
+
const command =
|
|
23
|
+
process.platform === "darwin"
|
|
24
|
+
? ["open", [url]]
|
|
25
|
+
: process.platform === "win32"
|
|
26
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
27
|
+
: ["xdg-open", [url]];
|
|
28
|
+
const child = spawn(command[0], command[1], {
|
|
29
|
+
detached: true,
|
|
30
|
+
stdio: "ignore",
|
|
31
|
+
});
|
|
32
|
+
child.unref();
|
|
33
|
+
child.on("error", () => {
|
|
34
|
+
process.stderr.write(
|
|
35
|
+
`lumine app-mcp: open this signed-in app tab: ${url}\n`,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function appMcpCommand(options) {
|
|
41
|
+
const buildId = resolveRequiredBuildId(options.target || options.buildIdFlag);
|
|
42
|
+
if (!buildId) {
|
|
43
|
+
throw new Error("Usage: lumine app-mcp <published-app-url-or-id>");
|
|
44
|
+
}
|
|
45
|
+
const auth = await resolveAuth(options);
|
|
46
|
+
await assertAuthScope({ options, auth, scope: "build:read" });
|
|
47
|
+
await assertAuthScope({ options, auth, scope: "build:write" });
|
|
48
|
+
const created = await createAppMcpSession({ options, auth, buildId });
|
|
49
|
+
const session = created?.session;
|
|
50
|
+
if (!session?.id || !session?.manifest?.tools?.length) {
|
|
51
|
+
if (session?.id) {
|
|
52
|
+
await closeAppMcpSession({
|
|
53
|
+
options,
|
|
54
|
+
auth,
|
|
55
|
+
buildId,
|
|
56
|
+
sessionId: session.id,
|
|
57
|
+
}).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
throw new Error("Twinkle did not return an app MCP session.");
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (options.openBrowser !== false) {
|
|
63
|
+
openApp(session.appUrl);
|
|
64
|
+
} else {
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
`lumine app-mcp: open this signed-in app tab: ${session.appUrl}\n`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
process.stderr.write(
|
|
70
|
+
`lumine app-mcp: ${session.buildTitle} is pinned to artifact ${session.artifactVersionId}. ` +
|
|
71
|
+
`Keep the opened Twinkle tab running.\n`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const tools = session.manifest.tools.map((tool) => ({
|
|
75
|
+
name: tool.name,
|
|
76
|
+
description: tool.description || "",
|
|
77
|
+
inputSchema: tool.inputSchema || {
|
|
78
|
+
type: "object",
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
const input = readline.createInterface({
|
|
83
|
+
input: process.stdin,
|
|
84
|
+
crlfDelay: Infinity,
|
|
85
|
+
terminal: false,
|
|
86
|
+
});
|
|
87
|
+
const pending = new Set();
|
|
88
|
+
let toolCallQueue = Promise.resolve();
|
|
89
|
+
input.on("line", (line) => {
|
|
90
|
+
if (!line.trim()) return;
|
|
91
|
+
const execute = () =>
|
|
92
|
+
handleMcpMessage({
|
|
93
|
+
line,
|
|
94
|
+
options,
|
|
95
|
+
auth,
|
|
96
|
+
buildId,
|
|
97
|
+
session,
|
|
98
|
+
tools,
|
|
99
|
+
});
|
|
100
|
+
let isToolCall = false;
|
|
101
|
+
try {
|
|
102
|
+
isToolCall = JSON.parse(line)?.method === "tools/call";
|
|
103
|
+
} catch {
|
|
104
|
+
// The normal handler returns the canonical JSON-RPC parse error.
|
|
105
|
+
}
|
|
106
|
+
// App mutations are stateful and the browser runtime can execute only one
|
|
107
|
+
// canonical call at a time. Preserve the MCP client's receive order so a
|
|
108
|
+
// burst never depends on UUID or second-resolution database ordering.
|
|
109
|
+
const operation = isToolCall
|
|
110
|
+
? (toolCallQueue = toolCallQueue.then(execute, execute))
|
|
111
|
+
: execute();
|
|
112
|
+
const observed = operation.catch((error) => {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`lumine app-mcp: ${String(error?.message || error)}\n`,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
pending.add(observed);
|
|
118
|
+
observed.finally(() => pending.delete(observed));
|
|
119
|
+
});
|
|
120
|
+
await new Promise((resolve) => input.once("close", resolve));
|
|
121
|
+
await Promise.allSettled(Array.from(pending));
|
|
122
|
+
} finally {
|
|
123
|
+
await closeAppMcpSession({
|
|
124
|
+
options,
|
|
125
|
+
auth,
|
|
126
|
+
buildId,
|
|
127
|
+
sessionId: session.id,
|
|
128
|
+
}).catch(() => {});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function callAppTool({
|
|
133
|
+
options,
|
|
134
|
+
auth,
|
|
135
|
+
buildId,
|
|
136
|
+
sessionId,
|
|
137
|
+
name,
|
|
138
|
+
arguments: toolArguments,
|
|
139
|
+
}) {
|
|
140
|
+
const created = await createAppMcpCall({
|
|
141
|
+
options,
|
|
142
|
+
auth,
|
|
143
|
+
buildId,
|
|
144
|
+
sessionId,
|
|
145
|
+
name,
|
|
146
|
+
arguments: toolArguments,
|
|
147
|
+
});
|
|
148
|
+
const callId = created?.call?.id;
|
|
149
|
+
if (!callId) throw new Error("Twinkle did not create the app tool call.");
|
|
150
|
+
const deadline = Date.now() + CALL_TIMEOUT_MS;
|
|
151
|
+
while (Date.now() < deadline) {
|
|
152
|
+
const payload = await loadAppMcpCall({
|
|
153
|
+
options,
|
|
154
|
+
auth,
|
|
155
|
+
buildId,
|
|
156
|
+
sessionId,
|
|
157
|
+
callId,
|
|
158
|
+
});
|
|
159
|
+
const call = payload?.call;
|
|
160
|
+
if (call?.status === "completed") return call.result;
|
|
161
|
+
if (call?.status === "failed") {
|
|
162
|
+
throw new Error(call.errorMessage || "App tool failed.");
|
|
163
|
+
}
|
|
164
|
+
await delay(CALL_POLL_MS);
|
|
165
|
+
}
|
|
166
|
+
const finalPayload = await loadAppMcpCall({
|
|
167
|
+
options,
|
|
168
|
+
auth,
|
|
169
|
+
buildId,
|
|
170
|
+
sessionId,
|
|
171
|
+
callId,
|
|
172
|
+
});
|
|
173
|
+
if (finalPayload?.call?.status === "completed") {
|
|
174
|
+
return finalPayload.call.result;
|
|
175
|
+
}
|
|
176
|
+
if (finalPayload?.call?.status === "failed") {
|
|
177
|
+
throw new Error(finalPayload.call.errorMessage || "App tool failed.");
|
|
178
|
+
}
|
|
179
|
+
throw new Error("App tool call timed out. Keep the MCP app tab open.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function handleMcpMessage({ line, options, auth, buildId, session, tools }) {
|
|
183
|
+
let message;
|
|
184
|
+
try {
|
|
185
|
+
message = JSON.parse(line);
|
|
186
|
+
} catch {
|
|
187
|
+
return writeMcpError(null, -32700, "Parse error");
|
|
188
|
+
}
|
|
189
|
+
const id = message?.id;
|
|
190
|
+
const method = String(message?.method || "");
|
|
191
|
+
if (id === undefined || id === null) return;
|
|
192
|
+
if (method === "initialize") {
|
|
193
|
+
return writeMcpResult(id, {
|
|
194
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
195
|
+
capabilities: { tools: { listChanged: false } },
|
|
196
|
+
serverInfo: {
|
|
197
|
+
name: `lumine-app-${buildId}`,
|
|
198
|
+
version: "1.0.0",
|
|
199
|
+
},
|
|
200
|
+
instructions:
|
|
201
|
+
session.manifest.description ||
|
|
202
|
+
`Use the semantic tools exposed by ${session.buildTitle}.`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (method === "ping") return writeMcpResult(id, {});
|
|
206
|
+
if (method === "tools/list") return writeMcpResult(id, { tools });
|
|
207
|
+
if (method === "tools/call") {
|
|
208
|
+
const name = String(message?.params?.name || "");
|
|
209
|
+
if (!tools.some((tool) => tool.name === name)) {
|
|
210
|
+
return writeMcpError(id, -32602, `Unknown tool: ${name}`);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const result = await callAppTool({
|
|
214
|
+
options,
|
|
215
|
+
auth,
|
|
216
|
+
buildId,
|
|
217
|
+
sessionId: session.id,
|
|
218
|
+
name,
|
|
219
|
+
arguments: message?.params?.arguments || {},
|
|
220
|
+
});
|
|
221
|
+
return writeMcpResult(id, {
|
|
222
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
223
|
+
structuredContent:
|
|
224
|
+
result && typeof result === "object" && !Array.isArray(result)
|
|
225
|
+
? result
|
|
226
|
+
: { result },
|
|
227
|
+
isError: false,
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return writeMcpResult(id, {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: JSON.stringify({
|
|
235
|
+
ok: false,
|
|
236
|
+
error: String(error?.message || error),
|
|
237
|
+
}),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
isError: true,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return writeMcpError(id, -32601, `Method not found: ${method}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function writeMcpResult(id, result) {
|
|
248
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function writeMcpError(id, code, message) {
|
|
252
|
+
process.stdout.write(
|
|
253
|
+
`${JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } })}\n`,
|
|
254
|
+
);
|
|
255
|
+
}
|
package/lib/commands.js
CHANGED
|
@@ -66,6 +66,7 @@ import { adminCommand } from "./admin.js";
|
|
|
66
66
|
import { runBuildForumCommand } from "./forum.js";
|
|
67
67
|
import { agentCommand } from "./agent.js";
|
|
68
68
|
import { agentMcpCommand } from "./agent/mcp-server.js";
|
|
69
|
+
import { appMcpCommand } from "./app-mcp/server.js";
|
|
69
70
|
import {
|
|
70
71
|
defaultMainCheckoutDir,
|
|
71
72
|
defaultReferenceDir,
|
|
@@ -111,6 +112,10 @@ export async function main() {
|
|
|
111
112
|
await agentMcpCommand(options);
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
115
|
+
if (options.command === "app-mcp") {
|
|
116
|
+
await appMcpCommand(options);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
114
119
|
options.lumineCli = await loadLumineCliVersionInfo({ options });
|
|
115
120
|
if (options.help) {
|
|
116
121
|
printHelp();
|
|
@@ -1104,7 +1109,13 @@ export async function save(options) {
|
|
|
1104
1109
|
lastSavedAt: new Date().toISOString(),
|
|
1105
1110
|
filesHash: typeof result.filesHash === "string" ? result.filesHash : null,
|
|
1106
1111
|
});
|
|
1107
|
-
printSaveResult({
|
|
1112
|
+
printSaveResult({
|
|
1113
|
+
result,
|
|
1114
|
+
build,
|
|
1115
|
+
dir,
|
|
1116
|
+
files,
|
|
1117
|
+
publishRequested: Boolean(options.publish),
|
|
1118
|
+
});
|
|
1108
1119
|
|
|
1109
1120
|
if (options.publish) {
|
|
1110
1121
|
if (build?.canPublish === false) {
|
|
@@ -1119,6 +1130,9 @@ export async function save(options) {
|
|
|
1119
1130
|
} else {
|
|
1120
1131
|
console.log("Publish complete.");
|
|
1121
1132
|
}
|
|
1133
|
+
console.log(
|
|
1134
|
+
`Release status: ${publish.build?.releaseStatus?.state || "unknown"}`,
|
|
1135
|
+
);
|
|
1122
1136
|
console.log(`App: ${options.siteUrl}/app/${buildId}`);
|
|
1123
1137
|
}
|
|
1124
1138
|
}
|
|
@@ -2043,7 +2057,13 @@ export function printForkResult({ forkResult, pullResult }) {
|
|
|
2043
2057
|
printPullResult(pullResult);
|
|
2044
2058
|
}
|
|
2045
2059
|
|
|
2046
|
-
export function printSaveResult({
|
|
2060
|
+
export function printSaveResult({
|
|
2061
|
+
result,
|
|
2062
|
+
build,
|
|
2063
|
+
dir,
|
|
2064
|
+
files,
|
|
2065
|
+
publishRequested = false,
|
|
2066
|
+
}) {
|
|
2047
2067
|
const entryPath = result.projectManifest?.entryPath || "unknown";
|
|
2048
2068
|
const version = result.artifactVersion?.versionNumber
|
|
2049
2069
|
? ` v${result.artifactVersion.versionNumber}`
|
|
@@ -2054,7 +2074,8 @@ export function printSaveResult({ result, build, dir, files }) {
|
|
|
2054
2074
|
`Uploaded ${files.length} file${files.length === 1 ? "" : "s"} from ${dir}`,
|
|
2055
2075
|
);
|
|
2056
2076
|
console.log(`Entry: ${entryPath}`);
|
|
2057
|
-
|
|
2077
|
+
const willPublish = publishRequested && build?.canPublish !== false;
|
|
2078
|
+
if (!willPublish) console.log(`Release status: ${releaseState}`);
|
|
2058
2079
|
if (isContributionBranch(build) && build.canPublish === false) {
|
|
2059
2080
|
console.log(
|
|
2060
2081
|
'Next: notify the project owner with `lumine suggest branch "Ready for review"`.',
|
|
@@ -2062,7 +2083,7 @@ export function printSaveResult({ result, build, dir, files }) {
|
|
|
2062
2083
|
console.log(
|
|
2063
2084
|
"To offer this branch's thumbnail, run `lumine suggest thumbnail`.",
|
|
2064
2085
|
);
|
|
2065
|
-
} else {
|
|
2086
|
+
} else if (!willPublish) {
|
|
2066
2087
|
console.log(
|
|
2067
2088
|
"Next: run `lumine launch` to publish, or `lumine save --publish` next time.",
|
|
2068
2089
|
);
|
|
@@ -2668,6 +2689,7 @@ export function printHelp() {
|
|
|
2668
2689
|
lumine whoami
|
|
2669
2690
|
lumine logout
|
|
2670
2691
|
lumine agent --provider <codex|claude-code> "<build request>"
|
|
2692
|
+
lumine app-mcp <published-app-url-or-id> [--no-open]
|
|
2671
2693
|
lumine new [title]
|
|
2672
2694
|
lumine rename [title] [--target <twinkle-build-url-or-id>]
|
|
2673
2695
|
lumine describe [description] [--target <twinkle-build-url-or-id>]
|
package/lib/constants.js
CHANGED
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-08-
|
|
5
|
-
Generated: 2026-08-
|
|
3
|
+
Version: 1.35.0
|
|
4
|
+
Updated: 2026-08-18
|
|
5
|
+
Generated: 2026-08-18T06:06:02.345Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -113,6 +113,16 @@ files:read, user:read, users:read, dailyReflections:read, content:read, content:
|
|
|
113
113
|
- Use navigate() for routes inside the current Build and openContent() for Twinkle subjects, comments, apps, profiles, and other content pages.
|
|
114
114
|
- Example: await Twinkle.app.openContent('https://www.twin-kle.com/subjects/432');
|
|
115
115
|
|
|
116
|
+
### Twinkle.appTools
|
|
117
|
+
- async register({ handlers }) | scopes: none
|
|
118
|
+
- Returns: { success, session } when opened by lumine app-mcp
|
|
119
|
+
- Register the live iframe handlers for the published app's static MCP tool manifest.
|
|
120
|
+
- Tool discovery comes only from /app-tools.json in the pinned published artifact; runtime code cannot add or rename tools.
|
|
121
|
+
- Every declared tool needs a same-named handler. Outside an app-mcp session, registration stores the handlers and resolves with active:false without starting a relay.
|
|
122
|
+
- Handlers run serially inside the visible app iframe and should return the confirmed post-action state.
|
|
123
|
+
- Do not synthesize server-owned state; await Twinkle SDK mutations before returning.
|
|
124
|
+
- Example: await Twinkle.appTools.register({ handlers: { get_state: () => ({ view, data }), open_view: ({ view }) => navigateTo(view) } });
|
|
125
|
+
|
|
116
126
|
### Twinkle.preview
|
|
117
127
|
- getLayout() | scopes: none
|
|
118
128
|
- Returns: { mode, viewport, stage, safeInsets, playfield }
|
|
@@ -302,6 +312,16 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
302
312
|
- Example: await Twinkle.files.delete(assetId);
|
|
303
313
|
|
|
304
314
|
### Twinkle.ai
|
|
315
|
+
- async getUsagePolicy() | scopes: none
|
|
316
|
+
- Returns: BuildAiUsagePolicy | null
|
|
317
|
+
- Load the signed-in viewer's canonical current AI Energy battery policy.
|
|
318
|
+
- Signed-in viewers only.
|
|
319
|
+
- Returns canonical server state and does not consume AI Energy.
|
|
320
|
+
- Use energyPercent for a percentage meter and energySegments plus energySegmentsRemaining for segmented battery UI.
|
|
321
|
+
- The Build-safe response includes battery/day/usage fields only; account identity, email, risk, and recharge-eligibility metadata are not exposed to the app iframe.
|
|
322
|
+
- Successful AI calls return a newer aiUsagePolicy snapshot. Energy-related SDK errors expose the confirmed snapshot as error.aiUsagePolicy. Replace displayed state only from those confirmed values; do not decrement or synthesize battery state locally.
|
|
323
|
+
- Example: const policy = await Twinkle.ai.getUsagePolicy();
|
|
324
|
+
renderBattery(policy?.energyPercent, policy?.energySegmentsRemaining);
|
|
305
325
|
- async listPrompts() | scopes: none
|
|
306
326
|
- Returns: Array<{ id, title, description }>
|
|
307
327
|
- Legacy helper. Twinkle.ai.chat does not require promptId for default runtime text generation.
|
|
@@ -321,20 +341,25 @@ console.log(analysis.bestMove, analysis.evaluation, analysis.mate);
|
|
|
321
341
|
- Use this for in-app AI replies instead of creating or fetching app-local endpoints such as /api/chat.
|
|
322
342
|
- Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
|
|
323
343
|
const result = await Twinkle.ai.chat({ message, history: chatHistory, systemPrompt: 'You are a cheerful pirate helper who answers in one sentence.', onText: (text, meta) => renderReply(text), onStatus: (status) => setThinking(status === 'thinking') });
|
|
324
|
-
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, instructions, systemPrompt, webSearch } = {}) | scopes: none
|
|
325
|
-
- Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, webSearch, aiUsagePolicy }
|
|
326
|
-
- Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic,
|
|
344
|
+
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus } = {}) | scopes: none
|
|
345
|
+
- Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, requestedModel, webSearch, aiUsagePolicy }
|
|
346
|
+
- Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic, with optional live output/status callbacks and web search.
|
|
327
347
|
- Signed-in viewers only.
|
|
328
348
|
- Use this instead of asking Twinkle.ai.chat to return JSON.
|
|
329
349
|
- expectedStructure must be a JSON object that describes the exact returned object shape.
|
|
330
350
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
351
|
+
- Omit model to use the normal Lite/Medium/High routing. model accepts only claude-opus-5 or claude-fable-5, and either explicit model must be paired with thinkingMode: 'high'; unknown model IDs reject instead of silently falling back.
|
|
331
352
|
- thinkingMode low uses GPT-5.6 Luna and consumes the viewer's AI Energy from confirmed provider usage; its smaller model is usually cheaper than Medium or High.
|
|
332
353
|
- thinkingMode medium uses Grok 4.6 with medium reasoning and consumes normal AI Energy.
|
|
333
354
|
- thinkingMode high uses GPT-5.6 Sol with high reasoning and consumes high AI Energy.
|
|
334
|
-
-
|
|
355
|
+
- claude-opus-5 uses Anthropic adaptive High thinking. claude-fable-5 uses Anthropic xhigh thinking and normally consumes more AI Energy for comparable token use. Both debit confirmed provider usage at the High tier.
|
|
356
|
+
- Pass onStatus and/or onText to stream progress from the same structured generation. onStatus receives high-level phases such as thinking, searching_web, responding, validating, and completed.
|
|
357
|
+
- onText receives accumulated structured-output text plus { done, delta, requestId, status }. Partial output is intentionally incomplete and may include provider formatting; parse only when done is true, when the callback receives the canonical object serialized as JSON, and use the resolved object as the source of truth.
|
|
358
|
+
- Streaming exposes app-visible structured output and high-level phases, not private model chain-of-thought. Put a user-facing field such as producerNotes in expectedStructure when the app should display model-authored commentary from the same generation.
|
|
359
|
+
- When AI Energy is empty, every automatic or named model choice rejects before new provider work; there is no free fallback mode.
|
|
335
360
|
- Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Low/Lite Mode remains tool-free; explicitly forcing webSearch: true in Low Mode returns an error.
|
|
336
|
-
- The
|
|
337
|
-
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: '
|
|
361
|
+
- The server validates the final shape; automatic OpenAI/xAI routes can retry malformed output, while explicit Anthropic routes use native JSON Schema output. App code should still validate business-specific enum values.
|
|
362
|
+
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'high', model: 'claude-opus-5', prompt: 'Plan the next section from: ' + currentState, expectedStructure: { producerNotes: 'string', action: 'string', confidence: 0 }, onStatus: (phase) => showPhase(phase), onText: (partialJson, meta) => showStructuredProgress(partialJson, meta) });
|
|
338
363
|
- onChatStatus(listener) | scopes: none
|
|
339
364
|
- Returns: unsubscribe function
|
|
340
365
|
- Listen to shared runtime AI chat stream events.
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -73,10 +73,12 @@ members. The response returns the canonical bucket, including its id for
|
|
|
73
73
|
later `get` / `accounts add` / `note set` calls.
|
|
74
74
|
|
|
75
75
|
`accounts add` accepts 1-500 unique positive user IDs, preflights the complete
|
|
76
|
-
batch before writing, adds
|
|
76
|
+
batch before writing, adds one exact canonical user rule per requested account,
|
|
77
77
|
re-attributes current-day AI usage, and returns the canonical bucket members.
|
|
78
|
-
It
|
|
79
|
-
|
|
78
|
+
It never infers or adds email aliases: shared verified addresses can belong to
|
|
79
|
+
unrelated accounts, so email rules require a separate explicit operator action.
|
|
80
|
+
The command is idempotent to retry. The API records the real operator in the
|
|
81
|
+
private Lumine audit log; no public bot identity is involved.
|
|
80
82
|
|
|
81
83
|
`note set` records up to 255 characters of private operational context on the
|
|
82
84
|
canonical bucket. Use it to distinguish quota bookkeeping from moderation;
|