@lampdotdev/lamp 0.36.2-nightly.19 → 0.36.2-nightly.22
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 +2 -2
- package/Resources/skills/web-app/SKILL.md +4 -1
- package/cli/lamps/lamp-for-games.js +60 -13
- package/cli/lamps/lamp.js +60 -13
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/lamps/web-starter/lamp.js +60 -13
- package/cli/share.mjs +40 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,14 +26,14 @@ Mac app, `projectOf`), or any folder with an `index.html`; without one, the fold
|
|
|
26
26
|
| `lamp use key w 2` | Tries it as a person would, then looks: `click`, `type`, `key` (several at once with `w+d`), `drag`, `scroll`, `wait`, and in a game on Lamp's runtime `stick` and `button`, which hold Lamp's own controller as Lamp's app does (`window.__lampDrive`) and give it back when the person plays. Several moves with `then` between them go one after another, with one look at the end. |
|
|
27
27
|
| `lamp reload [--fresh]` | Opens the page again from its files, a game from where the player is, or with `--fresh` from its beginning (`window.lampFreshStart`). |
|
|
28
28
|
| `lamp stop` | Stops running it. |
|
|
29
|
-
| `lamp share --words "…"` | Puts the page on lamp.dev: anyone with the link plays it there and can send an idea. Again, it sends the page as it is now to the same link. |
|
|
29
|
+
| `lamp share --words "…"` | Puts the page on lamp.dev: anyone with the link plays it there and can send an idea. Again, it sends the page as it is now to the same link. The app's icon goes with it (`iconOf`: a square `icon.png` or `apple-touch-icon.png` beside the page, a PNG its index.html names as its icon, or Lamp's `.lamp/icon.png`, made 512 pixels a side when bigger), for its page and the browser's tab. |
|
|
30
30
|
| `lamp share` (a page of Lamp's) | A web app or game in a Lamp's own folder is lamp's to share like any other page, Lamp open or not, to the link that Lamp keeps for it: lamp takes the link from the Lamp's `Wish Links.json` (by the app's instance id in `.lamp/meta.json`, `lampsKeptLink`) the first time, and tells it what went after each share or `unshare` (`keepInLamp`), so its Share Link card shows the link as it is. `ideas` and `unshare` take the link the same way. |
|
|
31
31
|
| `lamp share` (any other app of Lamp's) | For a folder in Lamp's own (or a link to one) that lamp doesn't share as a page, shared by Lamp's app over its socket (`share_link`, `OutsideWork`): its Share Link card filled as it fills itself (`WishLinkCards.prepare`: the link's last words, the questions and what's new Lamp writes, the app's file, the GitHub release) and shared without its window, with `--line`, `--by`, `--asks`, `--notes`, `--no-file`, `--no-github`, `--put-on-github` in place of the card's own, and `--preview` to see it first. The link's secret stays in Lamp. Which Lamp is the one whose folder it is (`lampOwning`), or the one it was given to (`lamp open --in-lamp`, `givenTo`); any other folder is lamp's to share, whatever Lamp is running, and so is a given folder when its Lamp is too old to share for lamp. |
|
|
32
32
|
| `lamp ideas [in <n> \| dismiss <n> \| reply <n> \| done <n> \| email <address\|off>] [note]` | The ideas people playing the link have sent and what's been said since, saying what became of one, and answering whoever sent it. |
|
|
33
33
|
| `lamp unshare` | Takes the link down. |
|
|
34
34
|
| `lamp account [username] [--email address]` | The maker's account on lamp.dev, which their links go under (`lamp.dev/<username>/<app>`): with a username and an email it asks the site for one (`askAccount`), whose key lamp keeps with its state (`account.json`, by site) and sends with every share (`X-Lamp-Account`); the link in the email the site sends confirms it. With only an email that has an account, it signs in to it, with a key of its own. On its own, it says which it's signed in as. A share before the email's link is opened goes out as links always have (`/w/<id>-<name>`), and moves under the username at its next share. |
|
|
35
35
|
| `lamp play <link> [--to folder] [--newest]` | Plays an app shared on lamp.dev on this computer: the link's file (`latestOf`, its `.lampapp`), unpacked as `lamp unpack` does into a folder of its own in `~/Lamp Games` (or `--to`), with Lamp's own files beside its page, then opened as `lamp open` opens any folder. Its AI is answered by `lamp dev` (`ai.mjs`) in any browser, Safari included, where a shared page can't reach the computer. lamp remembers which folder each link went into (a file for each link under `played/` in its state), so playing it again goes on where it was; `--newest` takes the link's latest version into a new folder. |
|
|
36
|
-
| `lamp ai [--forget]` | Answers the AI of pages shared on lamp.dev, played in a browser on this computer, with the person's own Claude Code, Codex or OpenRouter key (`cli/local-ai.mjs`, the same answers as `ai.mjs` gives `lamp dev`), until it's stopped. It listens on this computer alone at a port lamp.js knows (`LOCAL_AI_PORT`, `NEARBY` in lamp.js), and answers only a link's own page (`<id in hex>.play.lamp.dev`, `playOrigin`): the first time a page uses the AI, the person is asked in the terminal, and the page keeps the token it's given. Whether there's an AI is answered without asking. The browser asks the person too, once, before a page may reach this computer, so the link's frame on lamp.dev is given that permission. Safari never lets the page reach it (Safari 27, checked against a stand-in on the port), so lamp.js tells someone playing in Safari to use Chrome, Lamp or `lamp play` instead. `--forget` takes back every page allowed. Lamp's app runs the same server while it's open (`sharedPages` in `calls.mjs`, `SharedPagesAI`): it asks the person in a dialog and answers with the AI they set up in Lamp, and the pages allowed are the same list; whichever of the two starts first answers. |
|
|
36
|
+
| `lamp ai [--forget]` | Answers the AI of pages shared on lamp.dev, played in a browser on this computer, with the person's own Claude Code, Codex or OpenRouter key (`cli/local-ai.mjs`, the same answers as `ai.mjs` gives `lamp dev`), until it's stopped. It listens on this computer alone at a port lamp.js knows (`LOCAL_AI_PORT`, `NEARBY` in lamp.js), and answers only a link's own page (`<id in hex>.play.lamp.dev`, `playOrigin`): the first time a page uses the AI, the person is asked in the terminal, and the page keeps the token it's given. Whether there's an AI is answered without asking. The browser asks the person too, once, before a page may reach this computer, so the link's frame on lamp.dev is given that permission; lamp.js reaches the port only once that's granted, the page has a token, or the person asks (`LampAI.connect()` from a button, or a real AI call), so the browser doesn't ask as the page loads. Safari never lets the page reach it (Safari 27, checked against a stand-in on the port), so lamp.js tells someone playing in Safari to use Chrome, Lamp or `lamp play` instead. `--forget` takes back every page allowed. Lamp's app runs the same server while it's open (`sharedPages` in `calls.mjs`, `SharedPagesAI`): it asks the person in a dialog and answers with the AI they set up in Lamp, and the pages allowed are the same list; whichever of the two starts first answers. |
|
|
37
37
|
| `lamp devices [add \| forget <name>]` | The person's devices lamp puts a page on: an Xbox in Developer Mode (those set up and those found on the network), and Android (lamp's emulators, and the phones, tablets and TVs plugged in with USB debugging on). `add` sets one up, a step at a time; `add android` downloads Android's tools. |
|
|
38
38
|
| `lamp open --on xbox` | Packages the page, installs it on the device and opens it there. `lamp look --on xbox` is the device's screen and the app's log there, `lamp stop --on xbox` ends it, and `lamp remove --on xbox` takes it off. |
|
|
39
39
|
| `lamp open --on android [--hidden]` | Builds Lamp's Android app around the page and opens it in lamp's Android emulator (`--on android-tv`: its TV emulator; a device of the person's by its name), with `look`, `stop` and `remove` as on any device, and `lamp use --on android` to tap, type and press there. `--hidden` starts the emulator with no window. |
|
|
@@ -20,7 +20,10 @@ opening comment for the whole list. What it gives:
|
|
|
20
20
|
on it, check `LampAI.status('text')` and, when it can't answer, show its `reason` as it's given: lamp.js words it
|
|
21
21
|
for where the page is (away from the Mac's Wi‑Fi, Lamp closed, a link on lamp.dev), so don't write your own. Where it
|
|
22
22
|
gives `help` (a page away from Lamp), add a link to it beside the reason, "How to turn on AI", so someone who has never
|
|
23
|
-
heard of Lamp finds out how.
|
|
23
|
+
heard of Lamp finds out how. Where it answers `connect: true` (a page on lamp.dev that hasn't reached this computer
|
|
24
|
+
yet), also show a small "Connect AI" button that calls `LampAI.connect(capability)` with the same kind of AI you asked
|
|
25
|
+
`status` about (`LampAI.connect('pictures')` for pictures) and use the answer it gives as `status`'s;
|
|
26
|
+
never call `connect()` as the page loads, since it makes the browser ask the person.
|
|
24
27
|
- **LampAI.act**: the person asks the app to do something in their own words, and the app's own reads and writes do
|
|
25
28
|
it; "Doing things for the person" below, with **LampVoice** and **LampAI.hear** for saying it.
|
|
26
29
|
- **LampStore**: what the app saves, as JSON on the device. Once something is saved, only ever add to it: a new field
|
|
@@ -254,6 +254,26 @@
|
|
|
254
254
|
var ua = (window.navigator && navigator.userAgent) || '';
|
|
255
255
|
return /Safari\//.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|Firefox|FxiOS/.test(ua);
|
|
256
256
|
}
|
|
257
|
+
var SAFARI_AI = 'AI here works in Chrome or in Lamp, or with Lamp\'s command line: lamp play and this page\'s link.';
|
|
258
|
+
|
|
259
|
+
// Reaching this computer makes the browser ask the person about it, so a shared page doesn't reach out just to
|
|
260
|
+
// see whether there's an AI: only once the browser already lets it, the page has paired before, or the person
|
|
261
|
+
// has asked for it (LampAI.connect, from a button, or a real AI call).
|
|
262
|
+
var nearbyAsked = false;
|
|
263
|
+
function nearbyAllowed() {
|
|
264
|
+
// Connected before in this browser (kept with the page's saved things): the browser has had its say already.
|
|
265
|
+
if (nearbyAsked || read('nearby.token', null) || read('nearby.connected', false)) return Promise.resolve(true);
|
|
266
|
+
var permissions = window.navigator && navigator.permissions;
|
|
267
|
+
if (!permissions || typeof permissions.query !== 'function') return Promise.resolve(false);
|
|
268
|
+
// Chrome has named this permission more than one way; any that's granted will do, and one it doesn't know is unknown.
|
|
269
|
+
var names = ['local-network-access', 'loopback-network', 'local-network'];
|
|
270
|
+
return Promise.all(names.map(function (name) {
|
|
271
|
+
try {
|
|
272
|
+
return Promise.resolve(permissions.query({ name: name })).then(function (s) { return !!s && s.state === 'granted'; }, function () { return false; });
|
|
273
|
+
} catch (e) { return false; }
|
|
274
|
+
})).then(function (states) { return states.indexOf(true) !== -1; });
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
function nearby(path, body, timeout, deadline) {
|
|
258
278
|
var text = JSON.stringify(body);
|
|
259
279
|
function go(t) { return exchange(NEARBY, path, t, text, deadline ? Math.max(1000, deadline - Date.now()) : timeout); }
|
|
@@ -276,7 +296,7 @@
|
|
|
276
296
|
return done.then(null, function (e) {
|
|
277
297
|
if (e instanceof Failure) throw e;
|
|
278
298
|
// Safari never lets a page on the web reach this computer this way, so Lamp or lamp ai open here wouldn't help.
|
|
279
|
-
if (safari()) throw new Failure(
|
|
299
|
+
if (safari()) throw new Failure(SAFARI_AI, null, AI_HELP);
|
|
280
300
|
throw new Failure('AI isn\'t available here. Open Lamp on this computer, or with Lamp\'s command line, run lamp ai.', null, AI_HELP);
|
|
281
301
|
});
|
|
282
302
|
}
|
|
@@ -738,31 +758,58 @@
|
|
|
738
758
|
* a feature depends on it, and show the feature with its `reason` as it's given rather than words of the app's
|
|
739
759
|
* own, or hiding it or failing at it, and where it gives `help` (a page away from Lamp), a link to it beside the
|
|
740
760
|
* reason: "How to turn on AI". A call that fails for that reason has `needsSetup`, or `help`, set on its error.
|
|
761
|
+
* On a page shared on lamp.dev that hasn't reached this computer yet, `status` asks nothing and answers
|
|
762
|
+
* `connect: true`: show a small "Connect AI" button beside its reason that calls `LampAI.connect(capability)` with
|
|
763
|
+
* the same kind of AI (`connect('pictures')` for pictures), and use what that gives as `status`'s answer. Never
|
|
764
|
+
* call `connect()` as the page loads.
|
|
741
765
|
*/
|
|
742
766
|
var LampAI = {
|
|
743
767
|
Failure: Failure,
|
|
744
768
|
|
|
745
769
|
/** Whether this kind of AI ('text', 'decisions', 'pictures', 'act' or 'hear') can answer now: { available, reason,
|
|
746
|
-
* help }, where `reason` is the short line to show when it can't, written for where the page is,
|
|
747
|
-
* it's there, the page that says how to turn AI on, to link to beside it
|
|
770
|
+
* help, connect }, where `reason` is the short line to show when it can't, written for where the page is, `help`,
|
|
771
|
+
* when it's there, the page that says how to turn AI on, to link to beside it, and `connect`, when true, that
|
|
772
|
+
* a button calling `LampAI.connect()` would find out (a page on lamp.dev, before it has reached this computer). */
|
|
748
773
|
status: function (capability) {
|
|
749
774
|
var no = function (reason, help) {
|
|
750
775
|
var said = { available: false, reason: reason || 'AI isn\'t set up for this here.' };
|
|
751
776
|
if (help) said.help = help;
|
|
752
777
|
return said;
|
|
753
778
|
};
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
return
|
|
779
|
+
if (!bridge && shared()) {
|
|
780
|
+
return nearbyAllowed().then(function (allowed) {
|
|
781
|
+
if (allowed) return ask(capability);
|
|
782
|
+
if (safari()) return no(SAFARI_AI, AI_HELP);
|
|
783
|
+
return { available: false, connect: true, reason: 'Connect to the AI on this computer to use this.', help: AI_HELP };
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return ask(capability);
|
|
787
|
+
function ask(capability) {
|
|
788
|
+
// 'act' is asked of /ai/act itself: available only when this lamp.js has it, the app around the page lets the
|
|
789
|
+
// route through (an older one answers Not found) and the AI that answers the app can take it. 'hear' the same.
|
|
790
|
+
if (capability === 'act' || capability === 'hear') {
|
|
791
|
+
return post('/ai/' + capability, { probe: true }, 10000).then(function (reply) {
|
|
792
|
+
return reply[capability] === true ? { available: true, reason: '' } : no(reply.reason || reply.why);
|
|
793
|
+
}, function (e) { return no(e && e.message, e && e.help); });
|
|
794
|
+
}
|
|
795
|
+
return post('/ai/status', {}, 10000).then(function (reply) {
|
|
796
|
+
if (reply[capability] === true) return { available: true, reason: '' };
|
|
797
|
+
var detail = reply.details && reply.details[capability];
|
|
798
|
+
return no(detail && detail.reason);
|
|
759
799
|
}, function (e) { return no(e && e.message, e && e.help); });
|
|
760
800
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
/** On a page shared on lamp.dev, reaches the AI on this computer, so the browser asks the person then: call it
|
|
804
|
+
* from a button ("Connect AI") shown when `status` answers `connect: true`, never as the page loads. Gives what
|
|
805
|
+
* `status` gives for that kind of AI ('text' unless named). Anywhere else it's just `status`. */
|
|
806
|
+
connect: function (capability) {
|
|
807
|
+
nearbyAsked = true;
|
|
808
|
+
return LampAI.status(capability || 'text').then(function (said) {
|
|
809
|
+
// Reached it (whatever it said of this kind of AI): later visits look without asking first.
|
|
810
|
+
if (said.available || !said.help) write('nearby.connected', true);
|
|
811
|
+
return said;
|
|
812
|
+
});
|
|
766
813
|
},
|
|
767
814
|
|
|
768
815
|
/** Whether this kind of AI can answer now, as `status` says. */
|
package/cli/lamps/lamp.js
CHANGED
|
@@ -254,6 +254,26 @@
|
|
|
254
254
|
var ua = (window.navigator && navigator.userAgent) || '';
|
|
255
255
|
return /Safari\//.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|Firefox|FxiOS/.test(ua);
|
|
256
256
|
}
|
|
257
|
+
var SAFARI_AI = 'AI here works in Chrome or in Lamp, or with Lamp\'s command line: lamp play and this page\'s link.';
|
|
258
|
+
|
|
259
|
+
// Reaching this computer makes the browser ask the person about it, so a shared page doesn't reach out just to
|
|
260
|
+
// see whether there's an AI: only once the browser already lets it, the page has paired before, or the person
|
|
261
|
+
// has asked for it (LampAI.connect, from a button, or a real AI call).
|
|
262
|
+
var nearbyAsked = false;
|
|
263
|
+
function nearbyAllowed() {
|
|
264
|
+
// Connected before in this browser (kept with the page's saved things): the browser has had its say already.
|
|
265
|
+
if (nearbyAsked || read('nearby.token', null) || read('nearby.connected', false)) return Promise.resolve(true);
|
|
266
|
+
var permissions = window.navigator && navigator.permissions;
|
|
267
|
+
if (!permissions || typeof permissions.query !== 'function') return Promise.resolve(false);
|
|
268
|
+
// Chrome has named this permission more than one way; any that's granted will do, and one it doesn't know is unknown.
|
|
269
|
+
var names = ['local-network-access', 'loopback-network', 'local-network'];
|
|
270
|
+
return Promise.all(names.map(function (name) {
|
|
271
|
+
try {
|
|
272
|
+
return Promise.resolve(permissions.query({ name: name })).then(function (s) { return !!s && s.state === 'granted'; }, function () { return false; });
|
|
273
|
+
} catch (e) { return false; }
|
|
274
|
+
})).then(function (states) { return states.indexOf(true) !== -1; });
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
function nearby(path, body, timeout, deadline) {
|
|
258
278
|
var text = JSON.stringify(body);
|
|
259
279
|
function go(t) { return exchange(NEARBY, path, t, text, deadline ? Math.max(1000, deadline - Date.now()) : timeout); }
|
|
@@ -276,7 +296,7 @@
|
|
|
276
296
|
return done.then(null, function (e) {
|
|
277
297
|
if (e instanceof Failure) throw e;
|
|
278
298
|
// Safari never lets a page on the web reach this computer this way, so Lamp or lamp ai open here wouldn't help.
|
|
279
|
-
if (safari()) throw new Failure(
|
|
299
|
+
if (safari()) throw new Failure(SAFARI_AI, null, AI_HELP);
|
|
280
300
|
throw new Failure('AI isn\'t available here. Open Lamp on this computer, or with Lamp\'s command line, run lamp ai.', null, AI_HELP);
|
|
281
301
|
});
|
|
282
302
|
}
|
|
@@ -738,31 +758,58 @@
|
|
|
738
758
|
* a feature depends on it, and show the feature with its `reason` as it's given rather than words of the app's
|
|
739
759
|
* own, or hiding it or failing at it, and where it gives `help` (a page away from Lamp), a link to it beside the
|
|
740
760
|
* reason: "How to turn on AI". A call that fails for that reason has `needsSetup`, or `help`, set on its error.
|
|
761
|
+
* On a page shared on lamp.dev that hasn't reached this computer yet, `status` asks nothing and answers
|
|
762
|
+
* `connect: true`: show a small "Connect AI" button beside its reason that calls `LampAI.connect(capability)` with
|
|
763
|
+
* the same kind of AI (`connect('pictures')` for pictures), and use what that gives as `status`'s answer. Never
|
|
764
|
+
* call `connect()` as the page loads.
|
|
741
765
|
*/
|
|
742
766
|
var LampAI = {
|
|
743
767
|
Failure: Failure,
|
|
744
768
|
|
|
745
769
|
/** Whether this kind of AI ('text', 'decisions', 'pictures', 'act' or 'hear') can answer now: { available, reason,
|
|
746
|
-
* help }, where `reason` is the short line to show when it can't, written for where the page is,
|
|
747
|
-
* it's there, the page that says how to turn AI on, to link to beside it
|
|
770
|
+
* help, connect }, where `reason` is the short line to show when it can't, written for where the page is, `help`,
|
|
771
|
+
* when it's there, the page that says how to turn AI on, to link to beside it, and `connect`, when true, that
|
|
772
|
+
* a button calling `LampAI.connect()` would find out (a page on lamp.dev, before it has reached this computer). */
|
|
748
773
|
status: function (capability) {
|
|
749
774
|
var no = function (reason, help) {
|
|
750
775
|
var said = { available: false, reason: reason || 'AI isn\'t set up for this here.' };
|
|
751
776
|
if (help) said.help = help;
|
|
752
777
|
return said;
|
|
753
778
|
};
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
return
|
|
779
|
+
if (!bridge && shared()) {
|
|
780
|
+
return nearbyAllowed().then(function (allowed) {
|
|
781
|
+
if (allowed) return ask(capability);
|
|
782
|
+
if (safari()) return no(SAFARI_AI, AI_HELP);
|
|
783
|
+
return { available: false, connect: true, reason: 'Connect to the AI on this computer to use this.', help: AI_HELP };
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return ask(capability);
|
|
787
|
+
function ask(capability) {
|
|
788
|
+
// 'act' is asked of /ai/act itself: available only when this lamp.js has it, the app around the page lets the
|
|
789
|
+
// route through (an older one answers Not found) and the AI that answers the app can take it. 'hear' the same.
|
|
790
|
+
if (capability === 'act' || capability === 'hear') {
|
|
791
|
+
return post('/ai/' + capability, { probe: true }, 10000).then(function (reply) {
|
|
792
|
+
return reply[capability] === true ? { available: true, reason: '' } : no(reply.reason || reply.why);
|
|
793
|
+
}, function (e) { return no(e && e.message, e && e.help); });
|
|
794
|
+
}
|
|
795
|
+
return post('/ai/status', {}, 10000).then(function (reply) {
|
|
796
|
+
if (reply[capability] === true) return { available: true, reason: '' };
|
|
797
|
+
var detail = reply.details && reply.details[capability];
|
|
798
|
+
return no(detail && detail.reason);
|
|
759
799
|
}, function (e) { return no(e && e.message, e && e.help); });
|
|
760
800
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
/** On a page shared on lamp.dev, reaches the AI on this computer, so the browser asks the person then: call it
|
|
804
|
+
* from a button ("Connect AI") shown when `status` answers `connect: true`, never as the page loads. Gives what
|
|
805
|
+
* `status` gives for that kind of AI ('text' unless named). Anywhere else it's just `status`. */
|
|
806
|
+
connect: function (capability) {
|
|
807
|
+
nearbyAsked = true;
|
|
808
|
+
return LampAI.status(capability || 'text').then(function (said) {
|
|
809
|
+
// Reached it (whatever it said of this kind of AI): later visits look without asking first.
|
|
810
|
+
if (said.available || !said.help) write('nearby.connected', true);
|
|
811
|
+
return said;
|
|
812
|
+
});
|
|
766
813
|
},
|
|
767
814
|
|
|
768
815
|
/** Whether this kind of AI can answer now, as `status` says. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"platform":"mac","instance":"
|
|
1
|
+
{"platform":"mac","name":"Zqx App Name","instance":"09B1F6F7-D080-4175-9B91-C53B1B014BBF","exe":"ZqxAppName"}
|
|
@@ -254,6 +254,26 @@
|
|
|
254
254
|
var ua = (window.navigator && navigator.userAgent) || '';
|
|
255
255
|
return /Safari\//.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|Firefox|FxiOS/.test(ua);
|
|
256
256
|
}
|
|
257
|
+
var SAFARI_AI = 'AI here works in Chrome or in Lamp, or with Lamp\'s command line: lamp play and this page\'s link.';
|
|
258
|
+
|
|
259
|
+
// Reaching this computer makes the browser ask the person about it, so a shared page doesn't reach out just to
|
|
260
|
+
// see whether there's an AI: only once the browser already lets it, the page has paired before, or the person
|
|
261
|
+
// has asked for it (LampAI.connect, from a button, or a real AI call).
|
|
262
|
+
var nearbyAsked = false;
|
|
263
|
+
function nearbyAllowed() {
|
|
264
|
+
// Connected before in this browser (kept with the page's saved things): the browser has had its say already.
|
|
265
|
+
if (nearbyAsked || read('nearby.token', null) || read('nearby.connected', false)) return Promise.resolve(true);
|
|
266
|
+
var permissions = window.navigator && navigator.permissions;
|
|
267
|
+
if (!permissions || typeof permissions.query !== 'function') return Promise.resolve(false);
|
|
268
|
+
// Chrome has named this permission more than one way; any that's granted will do, and one it doesn't know is unknown.
|
|
269
|
+
var names = ['local-network-access', 'loopback-network', 'local-network'];
|
|
270
|
+
return Promise.all(names.map(function (name) {
|
|
271
|
+
try {
|
|
272
|
+
return Promise.resolve(permissions.query({ name: name })).then(function (s) { return !!s && s.state === 'granted'; }, function () { return false; });
|
|
273
|
+
} catch (e) { return false; }
|
|
274
|
+
})).then(function (states) { return states.indexOf(true) !== -1; });
|
|
275
|
+
}
|
|
276
|
+
|
|
257
277
|
function nearby(path, body, timeout, deadline) {
|
|
258
278
|
var text = JSON.stringify(body);
|
|
259
279
|
function go(t) { return exchange(NEARBY, path, t, text, deadline ? Math.max(1000, deadline - Date.now()) : timeout); }
|
|
@@ -276,7 +296,7 @@
|
|
|
276
296
|
return done.then(null, function (e) {
|
|
277
297
|
if (e instanceof Failure) throw e;
|
|
278
298
|
// Safari never lets a page on the web reach this computer this way, so Lamp or lamp ai open here wouldn't help.
|
|
279
|
-
if (safari()) throw new Failure(
|
|
299
|
+
if (safari()) throw new Failure(SAFARI_AI, null, AI_HELP);
|
|
280
300
|
throw new Failure('AI isn\'t available here. Open Lamp on this computer, or with Lamp\'s command line, run lamp ai.', null, AI_HELP);
|
|
281
301
|
});
|
|
282
302
|
}
|
|
@@ -738,31 +758,58 @@
|
|
|
738
758
|
* a feature depends on it, and show the feature with its `reason` as it's given rather than words of the app's
|
|
739
759
|
* own, or hiding it or failing at it, and where it gives `help` (a page away from Lamp), a link to it beside the
|
|
740
760
|
* reason: "How to turn on AI". A call that fails for that reason has `needsSetup`, or `help`, set on its error.
|
|
761
|
+
* On a page shared on lamp.dev that hasn't reached this computer yet, `status` asks nothing and answers
|
|
762
|
+
* `connect: true`: show a small "Connect AI" button beside its reason that calls `LampAI.connect(capability)` with
|
|
763
|
+
* the same kind of AI (`connect('pictures')` for pictures), and use what that gives as `status`'s answer. Never
|
|
764
|
+
* call `connect()` as the page loads.
|
|
741
765
|
*/
|
|
742
766
|
var LampAI = {
|
|
743
767
|
Failure: Failure,
|
|
744
768
|
|
|
745
769
|
/** Whether this kind of AI ('text', 'decisions', 'pictures', 'act' or 'hear') can answer now: { available, reason,
|
|
746
|
-
* help }, where `reason` is the short line to show when it can't, written for where the page is,
|
|
747
|
-
* it's there, the page that says how to turn AI on, to link to beside it
|
|
770
|
+
* help, connect }, where `reason` is the short line to show when it can't, written for where the page is, `help`,
|
|
771
|
+
* when it's there, the page that says how to turn AI on, to link to beside it, and `connect`, when true, that
|
|
772
|
+
* a button calling `LampAI.connect()` would find out (a page on lamp.dev, before it has reached this computer). */
|
|
748
773
|
status: function (capability) {
|
|
749
774
|
var no = function (reason, help) {
|
|
750
775
|
var said = { available: false, reason: reason || 'AI isn\'t set up for this here.' };
|
|
751
776
|
if (help) said.help = help;
|
|
752
777
|
return said;
|
|
753
778
|
};
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
return
|
|
779
|
+
if (!bridge && shared()) {
|
|
780
|
+
return nearbyAllowed().then(function (allowed) {
|
|
781
|
+
if (allowed) return ask(capability);
|
|
782
|
+
if (safari()) return no(SAFARI_AI, AI_HELP);
|
|
783
|
+
return { available: false, connect: true, reason: 'Connect to the AI on this computer to use this.', help: AI_HELP };
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return ask(capability);
|
|
787
|
+
function ask(capability) {
|
|
788
|
+
// 'act' is asked of /ai/act itself: available only when this lamp.js has it, the app around the page lets the
|
|
789
|
+
// route through (an older one answers Not found) and the AI that answers the app can take it. 'hear' the same.
|
|
790
|
+
if (capability === 'act' || capability === 'hear') {
|
|
791
|
+
return post('/ai/' + capability, { probe: true }, 10000).then(function (reply) {
|
|
792
|
+
return reply[capability] === true ? { available: true, reason: '' } : no(reply.reason || reply.why);
|
|
793
|
+
}, function (e) { return no(e && e.message, e && e.help); });
|
|
794
|
+
}
|
|
795
|
+
return post('/ai/status', {}, 10000).then(function (reply) {
|
|
796
|
+
if (reply[capability] === true) return { available: true, reason: '' };
|
|
797
|
+
var detail = reply.details && reply.details[capability];
|
|
798
|
+
return no(detail && detail.reason);
|
|
759
799
|
}, function (e) { return no(e && e.message, e && e.help); });
|
|
760
800
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
/** On a page shared on lamp.dev, reaches the AI on this computer, so the browser asks the person then: call it
|
|
804
|
+
* from a button ("Connect AI") shown when `status` answers `connect: true`, never as the page loads. Gives what
|
|
805
|
+
* `status` gives for that kind of AI ('text' unless named). Anywhere else it's just `status`. */
|
|
806
|
+
connect: function (capability) {
|
|
807
|
+
nearbyAsked = true;
|
|
808
|
+
return LampAI.status(capability || 'text').then(function (said) {
|
|
809
|
+
// Reached it (whatever it said of this kind of AI): later visits look without asking first.
|
|
810
|
+
if (said.available || !said.help) write('nearby.connected', true);
|
|
811
|
+
return said;
|
|
812
|
+
});
|
|
766
813
|
},
|
|
767
814
|
|
|
768
815
|
/** Whether this kind of AI can answer now, as `status` says. */
|
package/cli/share.mjs
CHANGED
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
13
13
|
import { deflateRawSync } from "node:zlib";
|
|
14
14
|
import { NOTES_FILE, Problem, stateOf } from "./state.mjs";
|
|
15
15
|
import { ANY_BROWSER, CONTROLLERS, CONTROLLERS_FILE, DEVICE_FILE, lampJS, LIBRARIES } from "./dev.mjs";
|
|
16
16
|
import { LIMITS, FILE_LIMITS, PAGE_LIMITS } from "../site/lib/shared-wishes.js";
|
|
17
17
|
import { ideasOf, makerOf, pageUp, publishLink, removeFile, removeIdea, removeLink, replyIdea, sendFile, siteOrigin } from "./link.mjs";
|
|
18
18
|
import { exportApp, sourceLinkOf } from "./lampapp.mjs";
|
|
19
|
+
import { scaled, sizeOf } from "./png.mjs";
|
|
19
20
|
|
|
20
21
|
export { siteOrigin };
|
|
21
22
|
|
|
@@ -175,6 +176,40 @@ const megabytes = (bytes) => `${(bytes / 1_000_000).toFixed(bytes < 10_000_000 ?
|
|
|
175
176
|
* are what the link says (`name`, `words`, and `line`, `by` and `asks` when given), `picture` a PNG or JPEG of the
|
|
176
177
|
* page or null, `shape` "fill" or the page's width over its height. Answers the link and what went up.
|
|
177
178
|
*/
|
|
179
|
+
/**
|
|
180
|
+
* The app's own icon for its link: a square PNG the page has (icon.png or apple-touch-icon.png beside it, or a PNG its
|
|
181
|
+
* index.html names as its icon), else the one Lamp keeps in its own app's folder (.lamp/icon.png). Made 512 pixels a side
|
|
182
|
+
* when it's bigger than the site takes. Null when there's none that's square: a page's own picture is never squashed.
|
|
183
|
+
*/
|
|
184
|
+
export function iconOf(layout) {
|
|
185
|
+
const names = ["icon.png", "apple-touch-icon.png"];
|
|
186
|
+
try {
|
|
187
|
+
const html = readFileSync(join(layout.root, "index.html"), "utf8");
|
|
188
|
+
for (const tag of html.match(/<link\b[^>]*>/gi) ?? []) {
|
|
189
|
+
const href = /\bhref=["']([^"'?#]+\.png)["']/i.exec(tag)?.[1];
|
|
190
|
+
if (/\brel=["'][^"']*\bicon\b/i.test(tag) && href && !/^[a-z]+:|^\/\//i.test(href)) names.push(href.replace(/^\.?\//, ""));
|
|
191
|
+
}
|
|
192
|
+
} catch {}
|
|
193
|
+
const candidates = [...names.map((name) => join(layout.root, name)), join(layout.folder, ".lamp", "icon.png")];
|
|
194
|
+
// Only a file really in the app's folder, links followed: a name like ../elsewhere.png or a link out of the folder
|
|
195
|
+
// would put someone's other picture on a public page.
|
|
196
|
+
let home;
|
|
197
|
+
try { home = realpathSync(layout.folder); } catch { return null; }
|
|
198
|
+
for (const file of candidates) {
|
|
199
|
+
let real;
|
|
200
|
+
try { real = realpathSync(file); } catch { continue; }
|
|
201
|
+
if (!real.startsWith(home + sep)) continue;
|
|
202
|
+
let bytes;
|
|
203
|
+
try { bytes = readFileSync(real); } catch { continue; }
|
|
204
|
+
const size = sizeOf(bytes);
|
|
205
|
+
if (!size || size.width !== size.height || size.width < 16) continue;
|
|
206
|
+
if (size.width <= 512 && bytes.length <= LIMITS.icon) return bytes;
|
|
207
|
+
const smaller = scaled(bytes, 512);
|
|
208
|
+
if (smaller && smaller.length <= LIMITS.icon) return smaller;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
178
213
|
export async function share({ layout, words, picture = null, shape = "fill", site = siteOrigin(), said = () => {}, file = true, anyway = false, github, fromLamp = false }) {
|
|
179
214
|
// The folder's own link when it has one on this site, changed in place; a link that's gone is made again.
|
|
180
215
|
let earlier = readLink(layout.folder);
|
|
@@ -200,6 +235,9 @@ export async function share({ layout, words, picture = null, shape = "fill", sit
|
|
|
200
235
|
...(words.asks?.length ? { asks: words.asks } : {}), ...(picture ? { picture: picture.toString("base64") } : {}),
|
|
201
236
|
...(repo ? { github: repo } : {}),
|
|
202
237
|
};
|
|
238
|
+
// The app's icon, which its page shows beside its name and in the browser's tab, as Lamp's Share Link sends it.
|
|
239
|
+
const icon = iconOf(layout);
|
|
240
|
+
if (icon) body.icon = icon.toString("base64");
|
|
203
241
|
// The link this app came from, when it's a copy of one shared there: its page says so.
|
|
204
242
|
const from = sourceLinkOf(layout.folder, site, earlier?.id);
|
|
205
243
|
if (from) body.from = from;
|