@lampdotdev/lamp 0.36.2-nightly.40 → 0.36.2-nightly.41
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 +1 -1
- package/cli/ai.mjs +163 -20
- package/cli/dev.mjs +4 -1
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/openrouter.mjs +12 -1
- package/cli/skill/SKILL.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ What a game's folder lacks all the same, the server gives it:
|
|
|
118
118
|
| `lamp-device.js`, `lamp-build.js` | Made by the server: the device is a browser on any device, as a page published to the web is given, and the build is named for the files as they are. |
|
|
119
119
|
| `lamp-controllers.js` | `cli/lamps/lamp-controllers.js`, the list of controllers as Lamp writes it (`Controllers.pageJS`). `CLIFilesTests` fails when the two differ, and writes the copy again when run with `LAMP_WRITE_CLI_FILES=1`. The game runtime can't load without it. |
|
|
120
120
|
| `three/`, `phaser/`, `rapier/` | Lamp's copies (`Resources/WebLibraries`), for a library the page asks for and the folder has none of. |
|
|
121
|
-
| `lamp.js` | Lamp's building blocks for a page (saved data, the app's files, AI through Lamp's app), as Lamp's own code writes them: `scripts/cli-files.sh` writes the file out (`cli/lamps/lamp.js`, and `lamp-for-games.js` as a game is given it, without a TV's remote; not committed, since they change often), and without it a stand-in that holds nothing. Its AI (`LampAI`: status, ask, act, decide) is answered by `lamp dev` itself (`ai.mjs`, through `lampGatewayHost` in `page.js`, `/__lamp/ai`, only for the page on this Mac) with the person's own Claude Code or Codex, or OpenRouter with their key in `OPENROUTER_API_KEY` (`LAMP_AI` picks one: claude, codex or openrouter; `LAMP_AI_MODEL` its model), the engines sealed as Lamp's app seals it (the same run, `complete` in ai.mjs)
|
|
121
|
+
| `lamp.js` | Lamp's building blocks for a page (saved data, the app's files, AI through Lamp's app), as Lamp's own code writes them: `scripts/cli-files.sh` writes the file out (`cli/lamps/lamp.js`, and `lamp-for-games.js` as a game is given it, without a TV's remote; not committed, since they change often), and without it a stand-in that holds nothing. Its AI (`LampAI`: status, ask, act, decide) is answered by `lamp dev` itself (`ai.mjs`, through `lampGatewayHost` in `page.js`, `/__lamp/ai`, only for the page on this Mac) with the person's own Claude Code or Codex, or OpenRouter with their key in `OPENROUTER_API_KEY` (`LAMP_AI` picks one: claude, codex or openrouter; `LAMP_AI_MODEL` its model), the engines sealed as Lamp's app seals it (the same run, `complete` in ai.mjs). Its pictures (`makeImage`, `editImage`, `editImages`) are made with the OpenRouter key when there is one (the app's own image model from `.lamp/models.json`, else Lamp's first), else by Codex's own image tool when Codex is one of the engines (`makeImage` in ai.mjs, as Lamp's app makes them); Claude Code can't make them, and a shared page's are made only with the OpenRouter key it was let use. Hearing needs Lamp's app. On a page shared on lamp.dev, Lamp's app or `lamp ai` answers it instead. |
|
|
122
122
|
|
|
123
123
|
A file the page asks for that neither the folder nor Lamp has is logged as an error, since a module that imports one
|
|
124
124
|
never runs and the page can't say so itself.
|
package/cli/ai.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// The AI a page asks for with LampAI (lamp.js), answered by lamp itself for the pages it serves, with the person's own
|
|
2
2
|
// Claude Code or Codex on this Mac: their subscription, through the program they signed in to, never a key in the page.
|
|
3
|
-
// The same routes and answers Lamp's app gives (`/ai/status`, `/ai/models`, `/ai`, `/ai/act`, `/ai/decide`),
|
|
4
|
-
// same code (app-ai.mjs, which Lamp's app has do it too), so a page can't tell which answered
|
|
5
|
-
//
|
|
3
|
+
// The same routes and answers Lamp's app gives (`/ai/status`, `/ai/models`, `/ai`, `/ai/act`, `/ai/decide`, `/ai/image`),
|
|
4
|
+
// told and checked by the same code (app-ai.mjs, which Lamp's app has do it too), so a page can't tell which answered
|
|
5
|
+
// it. Hearing needs Lamp's app: asked here, it says so.
|
|
6
6
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
-
import { accessSync, constants, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { accessSync, closeSync, constants, fstatSync, mkdtempSync, openSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { delimiter, join } from "node:path";
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
12
|
import { chatBody, decidePrompt, planPrompt, planSchema, readDecision, readMessage, readPlan, readStep } from "./app-ai.mjs";
|
|
13
|
-
import { DEFAULT_MODEL, OpenRouterTrouble, chat as openRouterChat, text as openRouterText } from "./openrouter.mjs";
|
|
13
|
+
import { DEFAULT_MODEL, IMAGE_MODELS, OpenRouterTrouble, chat as openRouterChat, image as openRouterImage, text as openRouterText } from "./openrouter.mjs";
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -115,13 +115,7 @@ export function complete({ system, prompt, schema, seconds = 60, which = engine(
|
|
|
115
115
|
try { event = JSON.parse(raw); } catch { return; }
|
|
116
116
|
if (which === "codex") {
|
|
117
117
|
if (event.type === "item.completed" && event.item?.type === "agent_message" && typeof event.item.text === "string") text = event.item.text;
|
|
118
|
-
else if (event
|
|
119
|
-
// Codex nests the service's error as JSON text: its message is what's said.
|
|
120
|
-
const words = String(event.message || event.error?.message || "Codex couldn't answer.");
|
|
121
|
-
let nested;
|
|
122
|
-
try { nested = JSON.parse(words)?.error?.message; } catch {}
|
|
123
|
-
trouble ??= new EngineTrouble("failed", nested || words);
|
|
124
|
-
}
|
|
118
|
+
else if (codexFailure(event)) trouble ??= new EngineTrouble("failed", codexFailure(event));
|
|
125
119
|
return;
|
|
126
120
|
}
|
|
127
121
|
if (event.type !== "result") return;
|
|
@@ -152,6 +146,16 @@ export function complete({ system, prompt, schema, seconds = 60, which = engine(
|
|
|
152
146
|
});
|
|
153
147
|
}
|
|
154
148
|
|
|
149
|
+
/** What Codex said went wrong in one of its events, or nothing. It nests the service's error as JSON text: its message
|
|
150
|
+
* is what's said. */
|
|
151
|
+
function codexFailure(event) {
|
|
152
|
+
if (event?.type !== "turn.failed" && event?.type !== "error") return "";
|
|
153
|
+
const words = String(event.message || event.error?.message || "Codex couldn't answer.");
|
|
154
|
+
let nested;
|
|
155
|
+
try { nested = JSON.parse(words)?.error?.message; } catch {}
|
|
156
|
+
return nested || words;
|
|
157
|
+
}
|
|
158
|
+
|
|
155
159
|
/** An answer from OpenRouter with the person's key (openrouter.mjs, as Lamp's app sends it), its troubles said as an engine's. */
|
|
156
160
|
async function viaOpenRouter({ system, prompt, schema, seconds, model, images, signal }) {
|
|
157
161
|
try {
|
|
@@ -166,15 +170,16 @@ async function viaOpenRouter({ system, prompt, schema, seconds, model, images, s
|
|
|
166
170
|
|
|
167
171
|
// MARK: Status and asking
|
|
168
172
|
|
|
169
|
-
/** `/ai/status`: what can answer here. Text, decisions and actions with an engine; pictures
|
|
170
|
-
export function status() {
|
|
173
|
+
/** `/ai/status`: what can answer here. Text, decisions and actions with an engine; pictures as `pictureRoute` says. */
|
|
174
|
+
export function status(page) {
|
|
171
175
|
const which = engine();
|
|
172
176
|
const route = which ? named(which) : null;
|
|
173
177
|
const available = (kind) => route ? { available: true, route, where: "remote" } : { available: false, reason: missing, setup: "lamp" };
|
|
178
|
+
const drawer = pictureRoute(page);
|
|
174
179
|
return {
|
|
175
|
-
text: !!route, decisions: !!route, pictures:
|
|
180
|
+
text: !!route, decisions: !!route, pictures: !!drawer, act: !!route,
|
|
176
181
|
details: { text: available("text"), decisions: available("decisions"), act: available("act"),
|
|
177
|
-
pictures: { available:
|
|
182
|
+
pictures: drawer ? { available: true, route: named(drawer), where: "remote" } : { available: false, reason: noPictures(page), setup: "lamp" } },
|
|
178
183
|
};
|
|
179
184
|
}
|
|
180
185
|
|
|
@@ -340,14 +345,152 @@ export async function act(body, here = engines()) {
|
|
|
340
345
|
return [502, { error: "The AI didn't answer." }];
|
|
341
346
|
}
|
|
342
347
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
348
|
+
// MARK: Pictures (`LampAI.makeImage`, `editImage`, `editImages`)
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* What makes a page's pictures here, as Lamp's app chooses (`AppAI.pictureRoute`): OpenRouter with the person's key,
|
|
352
|
+
* else Codex's own image tool when Codex is one of the engines here (Lamp's app spends Codex on pictures only when it
|
|
353
|
+
* was chosen), else nothing: Claude Code can't make pictures. A page shared on lamp.dev gets only the engine the person
|
|
354
|
+
* let it use (`enginesFor`), and never Codex, which makes a picture by running its own tools in a folder.
|
|
355
|
+
*/
|
|
356
|
+
export function pictureRoute(page) {
|
|
357
|
+
const here = enginesFor(page);
|
|
358
|
+
if (process.env.OPENROUTER_API_KEY && (!page || here[0] === "openrouter")) return "openrouter";
|
|
359
|
+
if (!page && here.includes("codex")) return "codex";
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Why nothing makes pictures here, saying what would. */
|
|
364
|
+
const noPictures = (page) => page
|
|
365
|
+
? "Pictures for a page shared on lamp.dev need an OpenRouter key here, in OPENROUTER_API_KEY with LAMP_AI=openrouter. Or open it with Lamp."
|
|
366
|
+
: "Making pictures here needs an OpenRouter key in OPENROUTER_API_KEY, or Codex on this computer, signed in. Or open the app in Lamp.";
|
|
367
|
+
|
|
368
|
+
/** How long a picture may take: as long as lamp.js waits for one, and what Lamp's app gives OpenRouter (calls.mjs). */
|
|
369
|
+
const PICTURE_SECONDS = 240;
|
|
370
|
+
|
|
371
|
+
/** The image model a picture is asked of: the app's own choice when it's one of Lamp's, else Lamp's first, as Lamp's app does. */
|
|
372
|
+
export const imageModelFor = (choice) => (IMAGE_MODELS.includes(choice) ? choice : IMAGE_MODELS[0]);
|
|
373
|
+
|
|
374
|
+
/** A picture's type by its first bytes, or nothing when it isn't one: what a model hands back is checked as a picture
|
|
375
|
+
* before a page is given it, as Lamp's app does. */
|
|
376
|
+
function pictureType(bytes) {
|
|
377
|
+
if (bytes.length > 8 && bytes[0] === 0x89 && bytes.toString("latin1", 1, 4) === "PNG") return "image/png";
|
|
378
|
+
if (bytes.length > 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "image/jpeg";
|
|
379
|
+
if (bytes.length > 12 && bytes.toString("latin1", 0, 4) === "RIFF" && bytes.toString("latin1", 8, 12) === "WEBP") return "image/webp";
|
|
380
|
+
return "";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* `/ai/image`: a picture made, or edited from the pictures the page gave (`images`, base64 with their `media_type`),
|
|
385
|
+
* answered as Lamp's app answers it: `{image, media_type}`, or an error, with `needs: "pictures"` when nothing here can
|
|
386
|
+
* make one. `app.imageModel` is the app's own choice of model (its .lamp/models.json). The request's size is the
|
|
387
|
+
* server's to cap, as every AI request's is.
|
|
388
|
+
*/
|
|
389
|
+
export async function makeImage(body, page, app = {}) {
|
|
390
|
+
const prompt = typeof body?.prompt === "string" ? body.prompt.trim() : "";
|
|
391
|
+
if (!prompt) return [400, { error: "Missing prompt." }];
|
|
392
|
+
const route = pictureRoute(page);
|
|
393
|
+
if (!route) return [501, { error: noPictures(page), needs: "pictures" }];
|
|
394
|
+
const references = (Array.isArray(body.images) ? body.images : [])
|
|
395
|
+
.filter((image) => typeof image?.data === "string" && image.data && typeof image.media_type === "string");
|
|
396
|
+
const aspectRatio = typeof body.aspect_ratio === "string" && body.aspect_ratio ? body.aspect_ratio : undefined;
|
|
397
|
+
let made;
|
|
398
|
+
try {
|
|
399
|
+
if (route === "openrouter") {
|
|
400
|
+
made = Buffer.from((await openRouterImage({ key: process.env.OPENROUTER_API_KEY, model: imageModelFor(app.imageModel), prompt,
|
|
401
|
+
references, aspectRatio, seconds: PICTURE_SECONDS })).image, "base64");
|
|
402
|
+
} else {
|
|
403
|
+
made = await codexImage({ prompt, references, aspectRatio });
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error?.kind === "timeout") return [504, { error: "The picture took too long to make." }];
|
|
407
|
+
return [502, { error: String(error?.reply?.error?.message || error.message) }];
|
|
408
|
+
}
|
|
409
|
+
const type = pictureType(made);
|
|
410
|
+
if (!type) return [502, { error: route === "codex" ? "Codex didn't make the picture. Check it's signed in." : "The image model didn't return a picture." }];
|
|
411
|
+
return [200, { image: made.toString("base64"), media_type: type }];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* What Codex may touch while it makes a picture: the scratch folder, and the little of the system its tools need to
|
|
416
|
+
* start, nothing of the person's and no network for its commands. Its image tool needs its code tools on, and the page
|
|
417
|
+
* writes the request, so the request is never trusted with more than the folder the picture comes back from.
|
|
418
|
+
*/
|
|
419
|
+
export function picturePermissions(folder) {
|
|
420
|
+
const filesystem = [[":root", "deny"], [":minimal", "read"], [folder, "write"]].map(([path, access]) => `${JSON.stringify(path)}=${JSON.stringify(access)}`);
|
|
421
|
+
return `permissions.lamp={filesystem={${filesystem.join(",")}},network={enabled=false}}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* A picture from Codex's built-in image tool, asked for as Lamp's app asks (`viaCodexImage`): run in a scratch folder
|
|
426
|
+
* it may write in and nothing outside it may be read, with none of the person's config, rules or hooks, told to save image.png there; the pictures to
|
|
427
|
+
* build on are attached with `-i` and named in the request. The folder goes when it's done. Takes about a minute.
|
|
428
|
+
*/
|
|
429
|
+
export function codexImage({ prompt, references = [], aspectRatio, seconds = PICTURE_SECONDS, tool = onPath("codex") || "codex" }) {
|
|
430
|
+
// By its real path: the profile below names it, and Codex refuses a folder reached through a link (/var is one).
|
|
431
|
+
const folder = realpathSync(mkdtempSync(join(tmpdir(), "lamp-image-")));
|
|
432
|
+
const files = references.map((image, index) => {
|
|
433
|
+
const name = `reference${index + 1}.${image.media_type.endsWith("png") ? "png" : "jpg"}`;
|
|
434
|
+
writeFileSync(join(folder, name), Buffer.from(image.data, "base64"));
|
|
435
|
+
return name;
|
|
436
|
+
});
|
|
437
|
+
let request = `Create an image and save it as image.png in the current folder, then reply with just: done.\n\nThe image: ${prompt}`;
|
|
438
|
+
if (aspectRatio) request += `\n\nAspect ratio ${aspectRatio}.`;
|
|
439
|
+
if (files.length) {
|
|
440
|
+
request += `\n\nUse the attached ${files.length === 1 ? "image" : "images"} (${files.join(", ")}) as `
|
|
441
|
+
+ (files.length === 1 ? "the reference: edit or build on it as the description says." : "references.");
|
|
442
|
+
}
|
|
443
|
+
const args = ["exec", "--json", "--ignore-user-config", "--ignore-rules", "--skip-git-repo-check", "--ephemeral",
|
|
444
|
+
"-c", picturePermissions(folder), "-c", 'default_permissions="lamp"', "-c", 'approval_policy="never"', "-c", "hooks={}", "-C", folder, ...files.flatMap((name) => ["-i", name]), "--", request];
|
|
445
|
+
// As `complete` runs it: never told to be Bun, which lamp itself is told.
|
|
446
|
+
const env = { ...process.env };
|
|
447
|
+
delete env.BUN_BE_BUN;
|
|
448
|
+
return new Promise((resolve, reject) => {
|
|
449
|
+
const child = spawn(tool, args, { cwd: folder, env, detached: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
450
|
+
let trouble = null, rest = "";
|
|
451
|
+
// The whole group, since Codex may leave a child of its own running.
|
|
452
|
+
const timer = setTimeout(() => {
|
|
453
|
+
trouble = new EngineTrouble("timeout", "The picture took too long to make.");
|
|
454
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch { child.kill("SIGKILL"); }
|
|
455
|
+
}, seconds * 1000);
|
|
456
|
+
const line = (raw) => {
|
|
457
|
+
let event;
|
|
458
|
+
try { event = JSON.parse(raw); } catch { return; }
|
|
459
|
+
if (codexFailure(event)) trouble ??= new EngineTrouble("failed", codexFailure(event));
|
|
460
|
+
};
|
|
461
|
+
child.stdout.setEncoding("utf8");
|
|
462
|
+
child.stdout.on("data", (chunk) => { rest += chunk; const lines = rest.split("\n"); rest = lines.pop(); lines.forEach(line); });
|
|
463
|
+
const finish = () => {
|
|
464
|
+
clearTimeout(timer);
|
|
465
|
+
let made = null;
|
|
466
|
+
// Never through a link: Codex may make image.png a link to a file it couldn't read itself, which lamp could.
|
|
467
|
+
let fd = null;
|
|
468
|
+
try {
|
|
469
|
+
fd = openSync(join(folder, "image.png"), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
470
|
+
if (fstatSync(fd).isFile()) made = readFileSync(fd);
|
|
471
|
+
} catch {} finally { if (fd !== null) closeSync(fd); }
|
|
472
|
+
rmSync(folder, { recursive: true, force: true });
|
|
473
|
+
return made;
|
|
474
|
+
};
|
|
475
|
+
child.on("error", () => { finish(); reject(new EngineTrouble("missing", "Codex isn't on this computer.")); });
|
|
476
|
+
child.on("close", () => {
|
|
477
|
+
if (rest) line(rest);
|
|
478
|
+
const made = finish();
|
|
479
|
+
if (trouble) return reject(trouble);
|
|
480
|
+
resolve(made ?? Buffer.alloc(0));
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** A page's AI request, by its route, as Lamp's app answers it: [status, JSON]. `app` is what the app chose for itself
|
|
486
|
+
* (`imageModel`), when that's known. */
|
|
487
|
+
export async function answer(path, body, page, app = {}) {
|
|
488
|
+
if (path === "/ai/status") return [200, status(page)];
|
|
346
489
|
if (path === "/ai/models") return [200, models(enginesFor(page))];
|
|
347
490
|
if (path === "/ai") return ask(body, enginesFor(page));
|
|
348
491
|
if (path === "/ai/act") return act(body, enginesFor(page));
|
|
349
492
|
if (path === "/ai/decide") return decide(body);
|
|
350
493
|
if (path === "/ai/hear") return body?.probe === true ? [200, { hear: false, why: "Hearing needs Lamp's app." }] : [501, { error: "Hearing needs Lamp's app: open the app in Lamp." }];
|
|
351
|
-
if (path === "/ai/image") return
|
|
494
|
+
if (path === "/ai/image") return makeImage(body, page, app);
|
|
352
495
|
return [404, { error: "Not found." }];
|
|
353
496
|
}
|
package/cli/dev.mjs
CHANGED
|
@@ -688,7 +688,10 @@ export async function serve({ folder: given, port: asked, lan = false, quiet = f
|
|
|
688
688
|
const path = typeof asked?.path === "string" ? asked.path : "";
|
|
689
689
|
let given = {};
|
|
690
690
|
try { given = typeof asked?.body === "string" ? JSON.parse(asked.body) : asked?.body ?? {}; } catch {}
|
|
691
|
-
|
|
691
|
+
// The app's own choice of image model, which Lamp's app keeps for it (`AppModels`), read at each request as Lamp's app reads it.
|
|
692
|
+
let chosen = {};
|
|
693
|
+
try { chosen = JSON.parse(readFileSync(join(folder, ".lamp", "models.json"), "utf8")) ?? {}; } catch {}
|
|
694
|
+
const [status, reply] = await answerAI(path, given, undefined, { imageModel: chosen.image });
|
|
692
695
|
return json(200, { status, body: JSON.stringify(reply) });
|
|
693
696
|
}
|
|
694
697
|
if (request.method === "POST" && url.pathname.startsWith("/__lamp/")) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"
|
|
1
|
+
{"name":"Zqx App Name","instance":"81942121-46A7-4886-9515-F2C5D44CC566","exe":"ZqxAppName","platform":"mac"}
|
package/cli/openrouter.mjs
CHANGED
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
const ENDPOINT = "https://openrouter.ai/api/v1/chat/completions";
|
|
7
7
|
/** The model lamp answers with when none is named: the one Lamp's app offers first for apps' text. */
|
|
8
8
|
export const DEFAULT_MODEL = "anthropic/claude-sonnet-5.5";
|
|
9
|
-
|
|
9
|
+
/** The image models an app may choose for its pictures, by OpenRouter's ids, first the one used when it chose none:
|
|
10
|
+
* Lamp's app's list (`Icons.artModels`), kept the same by hand. */
|
|
11
|
+
export const IMAGE_MODELS = [
|
|
12
|
+
"google/gemini-3.1-flash-image",
|
|
13
|
+
"google/gemini-nano-banana-2.1",
|
|
14
|
+
"google/gemini-3-pro-image",
|
|
15
|
+
"openai/gpt-image-2.5-sunburst",
|
|
16
|
+
"openai/gpt-image-2.5-flare",
|
|
17
|
+
"openai/gpt-image-2",
|
|
18
|
+
"google/gemini-3.1-flash-lite-image",
|
|
19
|
+
];
|
|
20
|
+
const FILTERED ="The AI provider's content filter cut the reply off. Try rewording the request.";
|
|
10
21
|
|
|
11
22
|
/** Why OpenRouter gave no answer: `kind` "refused" (with its `status` and `reply`), "filtered", "timeout" or "stopped". */
|
|
12
23
|
export class OpenRouterTrouble extends Error {
|
package/cli/skill/SKILL.md
CHANGED
|
@@ -36,7 +36,7 @@ looked earlier, `--reopen` puts it right.
|
|
|
36
36
|
a page (saved data with `LampStore`, the app's files and more). Read Lamp's `web-app` skill before writing it, and
|
|
37
37
|
the comments in `lamp.js` for how each block is used; never edit `lamp.js`. Its AI (`LampAI`: ask, act, decide) is
|
|
38
38
|
answered by lamp itself on this Mac, with the person's own Claude Code or Codex (or OpenRouter, with their key in `OPENROUTER_API_KEY`), in every page lamp runs here (`dev`,
|
|
39
|
-
`open`, `look`, `use`)
|
|
39
|
+
`open`, `look`, `use`). Pictures (`LampAI.makeImage` and its edits) are made there with the OpenRouter key, or with Codex; not with Claude Code alone. Hearing needs Lamp's app. Shared on lamp.dev, it's answered on the computer the page is played on while Lamp is open there or `{{lamp}} ai` runs there in a terminal, once the person allows that page. Never put a key or a model provider's address in the page. A page that wants none of Lamp's needs no starting files: write its
|
|
40
40
|
`index.html` in an empty folder.
|
|
41
41
|
|
|
42
42
|
## A Mac app
|