@lampdotdev/lamp 0.35.0 → 0.36.2-nightly.16
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 +51 -11
- package/Resources/GameKit/apple/LampPage.swift +202 -35
- package/Resources/GameKit/apple/LampWebGame.swift +1 -1
- package/Resources/GameKit/page/lamp-live.js +672 -0
- package/Resources/GameKit/web/lamp-game.js +7 -1
- package/Resources/share-rules.json +64 -0
- package/Resources/skills/game/SKILL.md +2 -0
- package/Resources/skills/github-projects/SKILL.md +8 -5
- package/Resources/skills/web-app/SKILL.md +49 -15
- package/Resources/skills/web-or-native/SKILL.md +6 -1
- package/cli/ai.mjs +237 -0
- package/cli/app-ai.mjs +473 -0
- package/cli/browser.mjs +18 -2
- package/cli/calls.mjs +213 -9
- package/cli/dev.mjs +61 -14
- package/cli/lamp.mjs +347 -37
- package/cli/lampapp.mjs +9 -9
- package/cli/lamps/android-host/LampPageGateway.kt +2 -2
- package/cli/lamps/lamp-for-games.js +95 -36
- package/cli/lamps/lamp.js +95 -36
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/lamps/mac-starter/Sources/LampSupport.swift +403 -285
- package/cli/lamps/web-starter/lamp.js +95 -36
- package/cli/link.mjs +77 -13
- package/cli/local-ai.mjs +193 -0
- package/cli/openrouter.mjs +133 -0
- package/cli/outside.mjs +65 -2
- package/cli/page.js +160 -8
- package/cli/share.mjs +80 -12
- package/cli/skill/SKILL.md +35 -10
- package/cli/state.mjs +3 -0
- package/cli/update.mjs +117 -12
- package/package.json +1 -1
- package/site/lib/shared-wishes.js +848 -117
package/cli/skill/SKILL.md
CHANGED
|
@@ -34,8 +34,9 @@ looked earlier, `--reopen` puts it right.
|
|
|
34
34
|
`{{lamp}} new path/to/folder --web --name "Its Name"` makes a new web app's page from Lamp's starting files:
|
|
35
35
|
`index.html`, `style.css` and `app.js`, which are yours to write, `APP.md`, and `lamp.js`, Lamp's building blocks for
|
|
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
|
-
the comments in `lamp.js` for how each block is used; never edit `lamp.js`.
|
|
38
|
-
|
|
37
|
+
the comments in `lamp.js` for how each block is used; never edit `lamp.js`. Its AI (`LampAI`: ask, act, decide) is
|
|
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`); pictures and hearing need 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
|
|
39
40
|
`index.html` in an empty folder.
|
|
40
41
|
|
|
41
42
|
## A Mac app
|
|
@@ -98,12 +99,16 @@ here: keep its license file, its copyright headers and its author's credit on sc
|
|
|
98
99
|
|
|
99
100
|
1. **Show the person the app** once, when you start, unless they don't want to watch: `{{lamp}} open`. It opens a
|
|
100
101
|
window they can use and play in while you work, and returns at once.
|
|
101
|
-
2. **Change the files.**
|
|
102
|
+
2. **Change the files.** First say what you're changing, in a few words of theirs: `{{lamp}} changing "Rain on the
|
|
103
|
+
roads"`. Do it before every change, however small: the app shows it on the card it puts up as the change goes in,
|
|
104
|
+
and without it the card says only that it's updating. It stands until you say something new.
|
|
102
105
|
3. **Look**: `{{lamp}} look`. It waits for the change to go in, then answers how it went, a picture of the page (a
|
|
103
106
|
file: read it; the last forty are kept), the screen as text, and the errors and the log since your last look. Look
|
|
104
107
|
after every change, and read the errors and the picture before saying it works. `{{lamp}} look --reopen` opens the
|
|
105
108
|
page again from its beginning first, a game with its saves kept: what someone opening it sees. `--size 900x600`
|
|
106
109
|
looks at it as a window of that size shows it, for a layout that has to hold at any shape (with no window open).
|
|
110
|
+
When the answer says lamp found no Chrome, Edge, Brave or Chromium, there's no picture and `use` can't play the
|
|
111
|
+
page: tell the person, since only they can install one.
|
|
107
112
|
|
|
108
113
|
Nothing has to be running first. With no window open, `look` runs the app itself in a copy of the page nobody sees,
|
|
109
114
|
opened from the files as they are, so the first look has no changes to report. The copy goes on running between
|
|
@@ -175,14 +180,34 @@ every command here works on a web app, a game, a screen or a Mac app in one, and
|
|
|
175
180
|
|
|
176
181
|
Only when the person asks: a link is public, and anyone who has it can play the page.
|
|
177
182
|
|
|
178
|
-
- `{{lamp}} share --
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
- Start with `{{lamp}} share --preview`: it says what would go and sends nothing. Show the person, and share only once they
|
|
184
|
+
say so.
|
|
185
|
+
- **A web app or game of Lamp's own** (its folder is in Lamp's) is shared here like any other page, whether Lamp is open
|
|
186
|
+
or not, to the link Lamp already has for it, and `ideas` and `unshare` work on that link too. Run `{{lamp}} share` with
|
|
187
|
+
no words of your own (it keeps the link's), and give `--line`, `--by` or `--asks "a|b"` only for what the person
|
|
188
|
+
wants changed.
|
|
189
|
+
- **Any other app of Lamp's** (a Mac or iPhone app, a wish of several; the preview says it's going through Lamp) is
|
|
190
|
+
shared by Lamp, which must be open, filled as its Share Link card fills itself: its words, the questions and what's new
|
|
191
|
+
Lamp writes, its file, its GitHub release. Give `--notes "What's new|And more"` and the rest only for what the person
|
|
192
|
+
wants changed. Its ideas and taking it down are on Lamp's Share Link card.
|
|
193
|
+
- **Any other page:** `{{lamp}} share --words "What it is, in a sentence or a few"` puts it on lamp.dev and answers its
|
|
194
|
+
link. `--line` is a line under its name, `--asks "Found a bug?|What's missing?"` what the box under the page asks
|
|
195
|
+
people, and `--by "Their Name"` who it's by, when they want another name than the one it goes out with (the name they last
|
|
196
|
+
gave, or Lamp's, or their name on this computer, which the share says). A link goes under the person's username on
|
|
197
|
+
lamp.dev (`lamp.dev/<username>/<app>`) once they have one: the first time you share, ask them which username they want
|
|
198
|
+
and their email, and run `{{lamp}} account <username> --email <theirs>`; they confirm it from the email, and the link
|
|
199
|
+
moves there at its next share. `{{lamp}} account` says which they're signed in as. A page that's logging errors
|
|
200
|
+
isn't shared until they're put right. Again, after changes, it sends the page as it is now to the same link, with the
|
|
201
|
+
same words unless you give new ones.
|
|
202
|
+
- `{{lamp}} ideas` lists what people playing it have sent, each with the address of a picture of the app as they saw
|
|
203
|
+
it when they kept one in: look at it before building the idea. Once one is in the game and the game shared again,
|
|
184
204
|
`{{lamp}} ideas in <number>` says so, and whoever asked to hear is written to; `{{lamp}} ideas dismiss <number>` lets
|
|
185
|
-
one go.
|
|
205
|
+
one go. Words after the number are a note to whoever sent it, shown on the link's page and put in that email: write
|
|
206
|
+
one only when the person asks you to answer them, in their words. Senders can answer by email: `{{lamp}} ideas` shows
|
|
207
|
+
what's been said under each idea, and those they've answered; `{{lamp}} ideas reply <number> <words>` answers again,
|
|
208
|
+
and `{{lamp}} ideas done <number>` leaves one without a word. `{{lamp}} ideas --earlier` lists those seen to. `{{lamp}} ideas email <address>` sends new ideas and replies to the
|
|
209
|
+
person's own address too, once they confirm it from the email it sends (`off` stops them): only with an address
|
|
210
|
+
they gave you.
|
|
186
211
|
- `{{lamp}} unshare` takes the link down.
|
|
187
212
|
|
|
188
213
|
Tell the person the link, and that it's public.
|
package/cli/state.mjs
CHANGED
|
@@ -22,6 +22,9 @@ export class Problem extends Error {}
|
|
|
22
22
|
|
|
23
23
|
/** The runtime whose presence makes a page a game's (`GamePage.runtimeFile`). */
|
|
24
24
|
export const RUNTIME_FILE = "lamp-game.js";
|
|
25
|
+
// The app's notes, at the top of its page: what it is and how it's made, for whoever works on it. Never part of the
|
|
26
|
+
// page: saving it changes nothing a page runs, and a shared page goes without it.
|
|
27
|
+
export const NOTES_FILE = "APP.md";
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* How the folder is laid out: an app Lamp made keeps its page in Web/, and any other folder with an index.html is a
|
package/cli/update.mjs
CHANGED
|
@@ -1,32 +1,81 @@
|
|
|
1
|
-
// Whether a newer lamp is out
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Whether a newer lamp is out. For a lamp installed from npm (@lampdotdev/lamp): at most once a day a process of its own
|
|
2
|
+
// asks npm's registry, without holding up the command, and keeps what it heard; the next command says so in one line,
|
|
3
|
+
// once a day, with the command that updates it. lamp never updates a global install: it's the person's.
|
|
4
|
+
// For the copy in Lamp's app, run as the person's `lamp` (lamp.sh): lamp is published on npm at every change to main
|
|
5
|
+
// (`nightly`) as well as with each release (`latest`), much sooner than Lamp's app can be built, so at most once an hour
|
|
6
|
+
// the same background process fetches the newest of its channel into ~/.lamp/cli when it's newer than the app's own,
|
|
7
|
+
// and the launcher runs that one from then on, until the app has caught up. Lamp's own calls of lamp (LampCLI.swift)
|
|
8
|
+
// never do: the app and the lamp inside it are always the same.
|
|
5
9
|
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
|
-
import {
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
8
13
|
import { homedir } from "node:os";
|
|
9
14
|
import { dirname, join, sep } from "node:path";
|
|
10
15
|
import { fileURLToPath } from "node:url";
|
|
11
16
|
|
|
12
17
|
const NAME = "@lampdotdev/lamp";
|
|
13
|
-
const DAY = 24 * 60 * 60 * 1000;
|
|
18
|
+
const DAY = 24 * 60 * 60 * 1000, HOUR = 60 * 60 * 1000;
|
|
14
19
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
20
|
const cache = () => join(process.env.LAMP_DEV_HOME ? dirname(process.env.LAMP_DEV_HOME) : join(homedir(), ".lamp"), "update.json");
|
|
16
21
|
|
|
17
22
|
/** Whether this copy is one npm installed, and so one npm updates. */
|
|
18
23
|
const fromNpm = () => here.includes(`${sep}node_modules${sep}@lampdotdev${sep}lamp${sep}`);
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
// npm's package has its package.json at its top, beside cli/; Lamp's app carries lamp's own in cli/ (build.sh).
|
|
26
|
+
const own = () => {
|
|
27
|
+
for (const file of [join(here, "..", "package.json"), join(here, "package.json")]) {
|
|
28
|
+
try { const { name, version } = JSON.parse(readFileSync(file, "utf8")); if ((name === NAME || name === "lamp") && version) return version; } catch {}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The npm channel a copy run as the person's `lamp` from Lamp's app follows: Lamp Nightly's, `nightly`; Lamp's, `latest`.
|
|
35
|
+
* None for Lamp Dev (a build from source has its checkout's lamp) or any other copy.
|
|
36
|
+
*/
|
|
37
|
+
const channel = () => {
|
|
38
|
+
if (!process.env.LAMP_FROM_APP || !process.env.LAMP_LAUNCHER) return null;
|
|
39
|
+
const app = process.env.LAMP_APP ?? "";
|
|
40
|
+
return app === "dev.lamp.host.nightly" ? "nightly" : app === "dev.lamp.host" ? "latest" : null;
|
|
41
|
+
};
|
|
42
|
+
/** Where fetched copies are kept, and the launcher's note of which to run (lamp.sh reads it). */
|
|
43
|
+
const fetched = () => join(dirname(cache()), "cli");
|
|
21
44
|
const read = () => { try { return JSON.parse(readFileSync(cache(), "utf8")); } catch { return {}; } };
|
|
22
45
|
const keep = (state) => { try { mkdirSync(dirname(cache()), { recursive: true }); writeFileSync(cache(), JSON.stringify(state)); } catch {} };
|
|
23
46
|
|
|
24
|
-
/**
|
|
47
|
+
/** This lamp's version, and where it came from, for `lamp --version`. */
|
|
48
|
+
export function versionLine() {
|
|
49
|
+
const mine = own() ?? "unknown";
|
|
50
|
+
const app = process.env.LAMP_BUNDLED_VERSION;
|
|
51
|
+
if (process.env.LAMP_FROM_APP && app && app !== mine) return `lamp ${mine} (fetched from npm, newer than the ${app} in Lamp's app)`;
|
|
52
|
+
if (process.env.LAMP_FROM_APP) return `lamp ${mine} (in Lamp's app)`;
|
|
53
|
+
return `lamp ${mine}${fromNpm() ? "" : " (from a checkout)"}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether version a is newer than b, as semver orders them: by their numbers, a pre-release older than its release, and
|
|
58
|
+
* two pre-releases by their parts in turn, numbers as numbers (0.36.2-nightly.10 after 0.36.2-nightly.9). Build
|
|
59
|
+
* metadata (+…) says nothing about order.
|
|
60
|
+
*/
|
|
25
61
|
export function newer(a, b) {
|
|
26
|
-
const parts = (version) => {
|
|
62
|
+
const parts = (version) => {
|
|
63
|
+
const [main, ...rest] = String(version).split("+")[0].split("-");
|
|
64
|
+
return { numbers: main.split(".").map(Number), pre: rest.length ? rest.join("-").split(".") : null };
|
|
65
|
+
};
|
|
27
66
|
const x = parts(a), y = parts(b);
|
|
28
67
|
for (let i = 0; i < 3; i++) if ((x.numbers[i] || 0) !== (y.numbers[i] || 0)) return (x.numbers[i] || 0) > (y.numbers[i] || 0);
|
|
29
|
-
|
|
68
|
+
if (!x.pre || !y.pre) return !x.pre && !!y.pre;
|
|
69
|
+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
|
|
70
|
+
const p = x.pre[i], q = y.pre[i];
|
|
71
|
+
if (p === undefined || q === undefined) return q === undefined;
|
|
72
|
+
if (p === q) continue;
|
|
73
|
+
const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
|
|
74
|
+
if (pn && qn) return Number(p) > Number(q);
|
|
75
|
+
if (pn !== qn) return qn; // a word part outranks a number part
|
|
76
|
+
return p > q;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
30
79
|
}
|
|
31
80
|
|
|
32
81
|
/**
|
|
@@ -35,7 +84,15 @@ export function newer(a, b) {
|
|
|
35
84
|
* LAMP_NO_UPDATE_CHECK set.
|
|
36
85
|
*/
|
|
37
86
|
export function updateNotice() {
|
|
38
|
-
if (
|
|
87
|
+
if (process.env.CI || process.env.LAMP_NO_UPDATE_CHECK) return null;
|
|
88
|
+
if (channel()) {
|
|
89
|
+
const state = read(), now = Date.now();
|
|
90
|
+
if (!(state.fetchChecked > now - HOUR)) {
|
|
91
|
+
try { spawn(process.execPath, [fileURLToPath(import.meta.url), "--fetch", channel()], { detached: true, stdio: "ignore" }).unref(); } catch {}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (!fromNpm()) return null;
|
|
39
96
|
const mine = own(), state = read(), now = Date.now();
|
|
40
97
|
if (!(state.checked > now - DAY)) {
|
|
41
98
|
try { spawn(process.execPath, [fileURLToPath(import.meta.url), "--check"], { detached: true, stdio: "ignore" }).unref(); } catch {}
|
|
@@ -55,3 +112,51 @@ if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === "-
|
|
|
55
112
|
if (typeof latest === "string" && /^\d+\.\d+\.\d+/.test(latest)) keep({ ...read(), checked: Date.now(), latest });
|
|
56
113
|
} catch {}
|
|
57
114
|
}
|
|
115
|
+
|
|
116
|
+
// The background fetch, for the copy in Lamp's app: the newest lamp of its channel, when it's newer than the app's own,
|
|
117
|
+
// checked against npm's integrity hash, unpacked into ~/.lamp/cli/<version>, and named for the launcher with the app's
|
|
118
|
+
// version it's newer than, so an app updated since runs its own again.
|
|
119
|
+
if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === "--fetch") {
|
|
120
|
+
const tag = process.argv[3];
|
|
121
|
+
keep({ ...read(), fetchChecked: Date.now() });
|
|
122
|
+
const app = process.env.LAMP_BUNDLED_VERSION || own();
|
|
123
|
+
try {
|
|
124
|
+
const answer = await fetch(`https://registry.npmjs.org/${NAME.replace("/", "%2F")}/${encodeURIComponent(tag)}`, { signal: AbortSignal.timeout(8000) });
|
|
125
|
+
const { version, dist } = answer.ok ? await answer.json() : {};
|
|
126
|
+
const folder = join(fetched(), String(version));
|
|
127
|
+
if (typeof version === "string" && /^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/.test(version) && app && newer(version, app)
|
|
128
|
+
&& typeof dist?.tarball === "string" && /^sha512-/.test(dist?.integrity ?? "")) {
|
|
129
|
+
if (!existsSync(join(folder, "package", "cli", "lamp.mjs"))) {
|
|
130
|
+
const bytes = Buffer.from(await (await fetch(dist.tarball, { signal: AbortSignal.timeout(60000) })).arrayBuffer());
|
|
131
|
+
if (`sha512-${createHash("sha512").update(bytes).digest("base64")}` !== dist.integrity) throw new Error("integrity");
|
|
132
|
+
const part = `${folder}.part`;
|
|
133
|
+
rmSync(part, { recursive: true, force: true });
|
|
134
|
+
mkdirSync(part, { recursive: true });
|
|
135
|
+
writeFileSync(join(part, "lamp.tgz"), bytes);
|
|
136
|
+
if (spawnSync("tar", ["xzf", "lamp.tgz"], { cwd: part }).status !== 0) throw new Error("unpack");
|
|
137
|
+
rmSync(join(part, "lamp.tgz"));
|
|
138
|
+
rmSync(folder, { recursive: true, force: true });
|
|
139
|
+
renameSync(part, folder);
|
|
140
|
+
}
|
|
141
|
+
// The one used until now is retired as of now: its week starts here, not from when it was fetched.
|
|
142
|
+
let before = null;
|
|
143
|
+
try { before = readFileSync(join(fetched(), "use"), "utf8").split("\n")[1] || null; } catch {}
|
|
144
|
+
if (before && dirname(before) !== folder) { try { const now = new Date(); utimesSync(dirname(before), now, now); } catch {} }
|
|
145
|
+
writeFileSync(join(fetched(), "use"), `${app}\n${join(folder, "package")}\n`);
|
|
146
|
+
// Older ones go once they've been out of use a week (the launcher touches the one it runs, and the one retired above
|
|
147
|
+
// was touched as it was): a `lamp dev` started from one may run for days, reading its files (libraries,
|
|
148
|
+
// controllers) as it goes.
|
|
149
|
+
// Never one a process running now was started from (a `lamp dev` that has run past its week): its command line
|
|
150
|
+
// names the copy's lamp.mjs.
|
|
151
|
+
const running = spawnSync("ps", ["-axo", "command"], { encoding: "utf8" }).stdout ?? "";
|
|
152
|
+
for (const other of readdirSync(fetched())) {
|
|
153
|
+
if (other === "use" || other === String(version) || other.endsWith(".part")) continue;
|
|
154
|
+
const at = join(fetched(), other);
|
|
155
|
+
if (running.includes(`${at}${sep}`)) continue;
|
|
156
|
+
try { if (statSync(at).mtimeMs < Date.now() - 7 * DAY) rmSync(at, { recursive: true, force: true }); } catch {}
|
|
157
|
+
}
|
|
158
|
+
// The one in use is touched, so it's counted from when it was last named.
|
|
159
|
+
try { const now = new Date(); utimesSync(folder, now, now); } catch {}
|
|
160
|
+
}
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|