@lampdotdev/lamp 0.36.2-nightly.16 → 0.36.2-nightly.19
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 +4 -1
- package/Resources/skills/app-ai/SKILL.md +1 -1
- package/THIRD_PARTY_NOTICES.md +0 -26
- package/cli/app-ai.mjs +6 -8
- package/cli/calls.mjs +2 -2
- package/cli/lampapp.mjs +27 -3
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/share.mjs +14 -6
- package/package.json +1 -1
- package/site/lib/shared-wishes.js +22 -7
package/README.md
CHANGED
|
@@ -222,7 +222,10 @@ of the link's own.
|
|
|
222
222
|
errors isn't shared without `--anyway`.
|
|
223
223
|
- **The link's secret**, which is what lets it be changed or taken down, is kept with lamp's state for the folder
|
|
224
224
|
(`link.json`, readable only by the person), never in the folder.
|
|
225
|
-
- **
|
|
225
|
+
- **The app's file** goes too (`exportApp`, as `lamp export` makes it), as Lamp's Share Link card sends it, unless
|
|
226
|
+
`--no-file`. Its own place on its credits' chain names the link it went up with (`link`, `lamp.dev/w/<id>`).
|
|
227
|
+
- **Where it came from:** a copy of an app someone shared (`lamp play`, `lamp unpack`) has their link on its chain, so
|
|
228
|
+
its own share names that link (`from`, `sourceLinkOf`), and its page says "from" them.
|
|
226
229
|
- **`LAMP_SITE`** names another site to share to, for trying this against a copy of the site (`npm run dev` in
|
|
227
230
|
`site/` keeps links in memory). The tests run against the site's handlers the same way, and never reach lamp.dev.
|
|
228
231
|
|
|
@@ -86,7 +86,7 @@ else { status = result.summary }
|
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
- Show the feature where `await LampAI.status(.act)` says it's available, with what answers and where its `processing`
|
|
89
|
-
is (`remote` for a service, Claude Code and Codex included
|
|
89
|
+
is (`remote` for a service, Claude Code and Codex included). When it isn't, keep
|
|
90
90
|
the feature visible with its `reason` and a "Set up in Lamp" button (`LampAI.openSetup(for: .act)`), or "Open Lamp" /
|
|
91
91
|
"Update Lamp" when `setup` says so; keep the typed draft, and every manual control working. Check again when the app
|
|
92
92
|
comes back to the front, and never send the draft by itself after setup. Available is no guarantee: show the actual
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -28,32 +28,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
28
28
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
29
|
SOFTWARE.
|
|
30
30
|
|
|
31
|
-
## llama.cpp
|
|
32
|
-
|
|
33
|
-
https://github.com/ggml-org/llama.cpp (llama-server and its libraries)
|
|
34
|
-
|
|
35
|
-
MIT License
|
|
36
|
-
|
|
37
|
-
Copyright (c) 2023-2026 The ggml authors
|
|
38
|
-
|
|
39
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
40
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
41
|
-
in the Software without restriction, including without limitation the rights
|
|
42
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
43
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
44
|
-
furnished to do so, subject to the following conditions:
|
|
45
|
-
|
|
46
|
-
The above copyright notice and this permission notice shall be included in all
|
|
47
|
-
copies or substantial portions of the Software.
|
|
48
|
-
|
|
49
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
50
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
51
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
52
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
53
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
54
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
55
|
-
SOFTWARE.
|
|
56
|
-
|
|
57
31
|
## Lobe Icons
|
|
58
32
|
|
|
59
33
|
https://github.com/lobehub/lobe-icons (`@lobehub/icons-static-png`, the provider logos in Settings)
|
package/cli/app-ai.mjs
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// page's.
|
|
12
12
|
//
|
|
13
13
|
// Every route is asked one of two ways and read into the same calls-or-reply: a chat with OpenAI-style tools
|
|
14
|
-
// (OpenRouter
|
|
15
|
-
//
|
|
14
|
+
// (OpenRouter: `chatBody`, `readMessage`), or one prompt whose answer must fit the step's JSON shape (Claude Code, Codex:
|
|
15
|
+
// `planPrompt`, `planSchema`, `readPlan`).
|
|
16
16
|
|
|
17
17
|
export const MAX_TOOLS = 24, MAX_CALLS = 8, MAX_STEPS = 4, MAX_BODY = 256 * 1024, MAX_CHOICES = 4, MAX_CHOICE = 24;
|
|
18
18
|
/** The longest a step may take, whatever its route, before its call is taken to have stopped answering (`ACT_STEP_SECONDS`
|
|
@@ -320,8 +320,8 @@ function framing(r, tag) {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
/**
|
|
323
|
-
* An OpenAI-style chat with tools, Lamp's `reply` among them, for OpenRouter
|
|
324
|
-
*
|
|
323
|
+
* An OpenAI-style chat with tools, Lamp's `reply` among them, for OpenRouter, whose `model` it names:
|
|
324
|
+
* `require_parameters` keeps OpenRouter on providers that take tools and `tool_choice` as
|
|
325
325
|
* given, and the models offered for apps' text take only "auto" and "none" there, so an answer may also be words without
|
|
326
326
|
* a call, which is the reply; a step asked for a reply only says "none".
|
|
327
327
|
*/
|
|
@@ -369,15 +369,13 @@ export function planSchema(r) {
|
|
|
369
369
|
|
|
370
370
|
/**
|
|
371
371
|
* The step as one prompt, for a route that takes text and answers in `planSchema`'s shape: the tools described, then the
|
|
372
|
-
* turn so far, every earlier call and its result fenced as outside text.
|
|
373
|
-
* for a model nothing holds to it.
|
|
372
|
+
* turn so far, every earlier call and its result fenced as outside text.
|
|
374
373
|
*/
|
|
375
|
-
export function planPrompt(r, tag, retry
|
|
374
|
+
export function planPrompt(r, tag, retry) {
|
|
376
375
|
let { system, user: prompt } = framing(r, tag);
|
|
377
376
|
system += "\n\n" + PLAN;
|
|
378
377
|
if (r.final) system += '\nOnly a reply is possible now: answer {"reply": {...}}.';
|
|
379
378
|
else system += "\n\nThe app's tools:\n" + r.tools.map((tool) => `- ${tool.name}: ${tool.reads ? "Looks up, changes nothing. " : "Changes the app's data. "}${tool.about}\n Arguments: ${json(toolSchema(tool))}`).join("\n");
|
|
380
|
-
if (schemaInPrompt) system += "\n\nThe JSON Schema your answer must fit:\n" + json(planSchema(r));
|
|
381
379
|
r.steps.forEach((step, i) => {
|
|
382
380
|
prompt += `\n\nYour answer ${i + 1} called:\n` + json(step.calls.map((c) => ({ name: c.name, args: c.args })));
|
|
383
381
|
prompt += "\nWhat happened, in that order:\n" + fence(tag, json(step.calls.map((c) => ({ ...(step.results[c.id] ?? skipped), name: c.name }))));
|
package/cli/calls.mjs
CHANGED
|
@@ -423,12 +423,12 @@ export const CALLS = {
|
|
|
423
423
|
|
|
424
424
|
/**
|
|
425
425
|
* What a step's route is asked: `way` "chat" is a chat with tools (`{body}`, OpenRouter's own when `model` names one),
|
|
426
|
-
* "plan" one prompt held to the step's shape (`{system, prompt, schema}`)
|
|
426
|
+
* and "plan" one prompt held to the step's shape (`{system, prompt, schema}`).
|
|
427
427
|
*/
|
|
428
428
|
async actAsk(given) {
|
|
429
429
|
const step = given.step, tag = text(given, "tag"), retry = typeof given.retry === "string" ? given.retry : null;
|
|
430
430
|
if (given.way === "chat") return { body: chatBody(step, tag, retry, typeof given.model === "string" ? given.model : undefined) };
|
|
431
|
-
return { ...planPrompt(step, tag, retry
|
|
431
|
+
return { ...planPrompt(step, tag, retry), schema: planSchema(step) };
|
|
432
432
|
},
|
|
433
433
|
|
|
434
434
|
/** A step's answer read and checked: `message` from a chat with tools, or `plan`'s text. `{answer}` or `{problem}`. */
|
package/cli/lampapp.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
8
8
|
import { inflateRawSync } from "node:zlib";
|
|
9
9
|
import { Problem, RESOURCES, manifestOf, projectOf, stateOf } from "./state.mjs";
|
|
10
10
|
import { LIMITS, nameOf, zipOf } from "./share.mjs";
|
|
11
|
+
import { isID } from "../site/lib/shared-wishes.js";
|
|
11
12
|
|
|
12
13
|
// What a Lamp app file may hold, written once in Resources/share-rules.json, which Lamp's app reads too (`ShareRules`):
|
|
13
14
|
// the sizes another Lamp opens, the names a license goes by, and the shapes of keys a shared file never carries.
|
|
@@ -214,9 +215,10 @@ function identityOf(folder) {
|
|
|
214
215
|
* and lamp-app.json, which says what it is. Never Lamp's own files, which the Mac that opens it writes, nor anything
|
|
215
216
|
* hidden. Refused when a license its credits name is gone, when something in it is shaped like a key, or when it's
|
|
216
217
|
* more than another Lamp opens. Work its credits give no license for goes only with `anyway`: its author may not want
|
|
217
|
-
* it passed on.
|
|
218
|
+
* it passed on. `link` is the lamp.dev link the file goes up with (`share`), which this app's place in the chain carries,
|
|
219
|
+
* so a copy of it shared on can say which link it came from (`sourceLinkOf`).
|
|
218
220
|
*/
|
|
219
|
-
export function exportApp(given, { anyway = false } = {}) {
|
|
221
|
+
export function exportApp(given, { anyway = false, link = null } = {}) {
|
|
220
222
|
const project = projectOf(given), folder = project.folder, manifest = manifestOf(folder);
|
|
221
223
|
// Where the app's own record is (its credits, licenses, notes, files and the Swift beside its page): the folder, or
|
|
222
224
|
// for a site laid out as Lamp puts one on GitHub, `.lamp-wish/` beside the page (`StaticSite.layOut`).
|
|
@@ -291,7 +293,7 @@ export function exportApp(given, { anyway = false } = {}) {
|
|
|
291
293
|
|
|
292
294
|
// Whoever opens the file is remixing this app: it's added to the chain its credits carry, as one link if it's
|
|
293
295
|
// already the last (`Credits.remixed`).
|
|
294
|
-
const me = { name, wish: identityOf(folder) }, chain = Array.isArray(credits?.remixOf) ? credits.remixOf.filter((link) => link && typeof link.name === "string") : [];
|
|
296
|
+
const me = { name, wish: identityOf(folder), ...(link ? { link } : {}) }, chain = Array.isArray(credits?.remixOf) ? credits.remixOf.filter((link) => link && typeof link.name === "string") : [];
|
|
295
297
|
const last = chain.at(-1);
|
|
296
298
|
if (last && (last.wish ? last.wish === me.wish : !last.starter && last.name === me.name)) chain[chain.length - 1] = { ...last, ...me, url: last.url, commit: last.commit };
|
|
297
299
|
else chain.push(me);
|
|
@@ -324,6 +326,28 @@ export function exportApp(given, { anyway = false } = {}) {
|
|
|
324
326
|
return { name, kind, bytes, files: files.size };
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
/**
|
|
330
|
+
* The id of the link on `site` that the app in the folder came from, for a share to name (`from`): the last app in its
|
|
331
|
+
* credits' chain that went up with a link there (`exportApp`'s `link`), other than `own`, the folder's own link. Null for
|
|
332
|
+
* an app that came from no link. An unpacked copy keeps the app's identity, so its first share replaces that entry's
|
|
333
|
+
* link in the file it sends; the folder's own credits still name the one it came from.
|
|
334
|
+
*/
|
|
335
|
+
export function sourceLinkOf(folder, site, own = null) {
|
|
336
|
+
const app = existsSync(join(folder, ".lamp-wish", "lamp-app.json")) ? join(folder, ".lamp-wish") : folder;
|
|
337
|
+
let chain;
|
|
338
|
+
try { chain = JSON.parse(readFileSync(join(app, "credits.json"), "utf8"))?.remixOf; } catch { return null; }
|
|
339
|
+
if (!Array.isArray(chain)) return null;
|
|
340
|
+
let origin;
|
|
341
|
+
try { origin = new URL(site).origin; } catch { return null; }
|
|
342
|
+
for (const entry of [...chain].reverse()) {
|
|
343
|
+
let url;
|
|
344
|
+
try { url = new URL(entry?.link); } catch { continue; }
|
|
345
|
+
const id = /^\/w\/([^/]+)$/.exec(url.pathname)?.[1];
|
|
346
|
+
if (url.origin === origin && isID(id) && id !== own) return id;
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
327
351
|
/** An object with its keys in order, at every level, as Lamp's own encoder writes its files. */
|
|
328
352
|
function sortedKeys(value) {
|
|
329
353
|
if (Array.isArray(value)) return value.map(sortedKeys);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"
|
|
1
|
+
{"platform":"mac","instance":"12A4CA51-92CD-4391-8AD0-548165ABFCAE","exe":"ZqxAppName","name":"Zqx App Name"}
|
package/cli/share.mjs
CHANGED
|
@@ -15,11 +15,13 @@ 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
|
-
import { exportApp } from "./lampapp.mjs";
|
|
18
|
+
import { exportApp, sourceLinkOf } from "./lampapp.mjs";
|
|
19
19
|
|
|
20
20
|
export { siteOrigin };
|
|
21
21
|
|
|
22
22
|
const linkFileOf = (folder) => join(stateOf(folder), "link.json");
|
|
23
|
+
/** A link by its id alone, which the site sends on to its address now: what a file names, so a rename never breaks it. */
|
|
24
|
+
const stableLink = (site, id) => `${site}/w/${id}`;
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -174,9 +176,13 @@ const megabytes = (bytes) => `${(bytes / 1_000_000).toFixed(bytes < 10_000_000 ?
|
|
|
174
176
|
* page or null, `shape` "fill" or the page's width over its height. Answers the link and what went up.
|
|
175
177
|
*/
|
|
176
178
|
export async function share({ layout, words, picture = null, shape = "fill", site = siteOrigin(), said = () => {}, file = true, anyway = false, github, fromLamp = false }) {
|
|
179
|
+
// The folder's own link when it has one on this site, changed in place; a link that's gone is made again.
|
|
180
|
+
let earlier = readLink(layout.folder);
|
|
181
|
+
if (earlier && earlier.site !== site) earlier = null;
|
|
177
182
|
// The app's own file goes with the link, as Lamp's Share Link sends it, so whoever follows the link can open it in
|
|
178
|
-
// Lamp too: made first, with Share…'s checks, so a page that can't be shared (a key in it) puts nothing up.
|
|
179
|
-
|
|
183
|
+
// Lamp too: made first, with Share…'s checks, so a page that can't be shared (a key in it) puts nothing up. It names
|
|
184
|
+
// the link it goes up with, so a copy shared on says it came from this one: made again below if that's a new link.
|
|
185
|
+
let app = file ? exportApp(layout.folder, { anyway, link: earlier ? stableLink(site, earlier.id) : null }) : null;
|
|
180
186
|
const files = pageFiles(layout);
|
|
181
187
|
const unpacked = files.reduce((total, [, bytes]) => total + bytes.length, 0);
|
|
182
188
|
if (!files.some(([path]) => path === "index.html")) throw new Problem("the page has no index.html at its top");
|
|
@@ -194,9 +200,9 @@ export async function share({ layout, words, picture = null, shape = "fill", sit
|
|
|
194
200
|
...(words.asks?.length ? { asks: words.asks } : {}), ...(picture ? { picture: picture.toString("base64") } : {}),
|
|
195
201
|
...(repo ? { github: repo } : {}),
|
|
196
202
|
};
|
|
197
|
-
// The
|
|
198
|
-
|
|
199
|
-
if (
|
|
203
|
+
// The link this app came from, when it's a copy of one shared there: its page says so.
|
|
204
|
+
const from = sourceLinkOf(layout.folder, site, earlier?.id);
|
|
205
|
+
if (from) body.from = from;
|
|
200
206
|
// A file an earlier share sent stays on the record until it's taken away, so a retry after a failed removal tries again.
|
|
201
207
|
// A link that's a Lamp's (`fromLamp`, the app in its own folder) stays so in every write, a share that fails partway too.
|
|
202
208
|
let link = { site, ...(await publishLink({ site, body, earlier, account: accountFor(site)?.key })), ...words, shape, ...(earlier?.file ? { file: true } : {}),
|
|
@@ -204,6 +210,8 @@ export async function share({ layout, words, picture = null, shape = "fill", sit
|
|
|
204
210
|
// Kept before the page goes up: a link whose page didn't make it is still this folder's to send again or take down.
|
|
205
211
|
writeLink(layout.folder, link);
|
|
206
212
|
|
|
213
|
+
// A new link (or one made again) has its id only now, which the file carries.
|
|
214
|
+
if (app && link.id !== earlier?.id) app = exportApp(layout.folder, { anyway, link: stableLink(site, link.id) });
|
|
207
215
|
// The file, or none: a link that carried one and shouldn't now has it taken away (and its page with it, sent next).
|
|
208
216
|
if (app) {
|
|
209
217
|
link.file = true;
|
package/package.json
CHANGED
|
@@ -137,11 +137,11 @@ function release(value) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** What a share sends, checked. Null when anything is off. `releases` is undefined when the share didn't say (a Lamp
|
|
140
|
-
* from before them), so a change keeps the ones the link has. */
|
|
140
|
+
* from before them), so a change keeps the ones the link has; `from` too, the id of the link the app is a copy of. */
|
|
141
141
|
const GITHUB_REPO = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/(?!\.\.?$)[A-Za-z0-9._-]{1,100}$/;
|
|
142
142
|
export function readShare(body) {
|
|
143
143
|
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
144
|
-
const known = ["name", "by", "line", "words", "ideas", "asks", "shape", "releases", "picture", "icon", "github"];
|
|
144
|
+
const known = ["name", "by", "line", "words", "ideas", "asks", "shape", "releases", "picture", "icon", "github", "from"];
|
|
145
145
|
if (Object.keys(body).some((key) => !known.includes(key))) return null;
|
|
146
146
|
const name = tidy(body.name, LIMITS.name);
|
|
147
147
|
const words = tidy(body.words, LIMITS.words, { lines: true });
|
|
@@ -159,9 +159,10 @@ export function readShare(body) {
|
|
|
159
159
|
const icon = picture(body.icon, LIMITS.icon, { square: true });
|
|
160
160
|
// The app's public repository on GitHub, as "owner/name", which the page links to: nothing else is taken.
|
|
161
161
|
const github = body.github == null || body.github === "" ? null : GITHUB_REPO.test(body.github) ? body.github : undefined;
|
|
162
|
-
|
|
162
|
+
const from = body.from == null ? undefined : isID(body.from) ? body.from : null;
|
|
163
|
+
if (!name || !words || line == null || by == null || ideas.includes(null) || asks.includes(null) || shape === undefined || releases?.includes(null) || !shot || !icon || github === undefined || from === null) return null;
|
|
163
164
|
return { name, by, line, words, ideas, asks, shape, releases: releases?.sort((a, b) => b.date.localeCompare(a.date)),
|
|
164
|
-
picture: shot.bytes ? shot : null, icon: icon.bytes ? icon : null, github };
|
|
165
|
+
picture: shot.bytes ? shot : null, icon: icon.bytes ? icon : null, github, from };
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
/** What anyone may see of a shared wish: never its secret. */
|
|
@@ -772,8 +773,11 @@ export function createShareHandler({ env = process.env, fetcher = fetch, store,
|
|
|
772
773
|
const picture = share.picture && { url: await put("picture", share.picture), width: share.picture.width, height: share.picture.height };
|
|
773
774
|
const icon = share.icon && { url: await put("icon", share.icon) };
|
|
774
775
|
const time = now().toISOString();
|
|
776
|
+
// The link the app is a copy of, kept only while there's such a link and it isn't this one: a share that doesn't
|
|
777
|
+
// say, or names one that's gone, keeps what the record had.
|
|
778
|
+
const from = share.from && share.from !== wishID && (await kept.get(share.from)) ? share.from : null;
|
|
775
779
|
const words = { name: share.name, by: share.by, line: share.line, words: share.words, ideas: share.ideas, asks: share.asks, shape: share.shape, github: share.github,
|
|
776
|
-
...(share.releases ? { releases: share.releases } : {}), picture: picture || null, icon: icon || null };
|
|
780
|
+
...(share.releases ? { releases: share.releases } : {}), ...(from ? { from } : {}), picture: picture || null, icon: icon || null };
|
|
777
781
|
// Its name under the account stays what it was first given, whatever the app is called later.
|
|
778
782
|
const named = earlier?.owner ? { owner: earlier.owner, app: earlier.app }
|
|
779
783
|
: owner ? { owner, app: await claimAppName(kept, owner, words.name, wishID) } : {};
|
|
@@ -1287,10 +1291,21 @@ export function createIdeaMailHandler({ env = process.env, fetcher = fetch, stor
|
|
|
1287
1291
|
}
|
|
1288
1292
|
|
|
1289
1293
|
/** A shared wish for the page and its card, or null when there's none by that id (or it can't be read now). */
|
|
1290
|
-
export async function findWish(id, { env = process.env, fetcher = fetch, store } = {}) {
|
|
1294
|
+
export async function findWish(id, { env = process.env, fetcher = fetch, store, origin = null } = {}) {
|
|
1291
1295
|
if (!isID(id)) return null;
|
|
1292
|
-
|
|
1296
|
+
let kept, record;
|
|
1297
|
+
try { kept = store ?? cloudStore(env, fetcher); record = await kept.get(id); }
|
|
1293
1298
|
catch { return undefined; }
|
|
1299
|
+
const wish = shown(record);
|
|
1300
|
+
// Asked with the site's origin (the page): the link it came from, as that link is known now, `from` on its page. One
|
|
1301
|
+
// taken down since, or that can't be read just now, is left out.
|
|
1302
|
+
if (wish && origin && isID(record.from) && record.from !== id) {
|
|
1303
|
+
try {
|
|
1304
|
+
const source = await kept.get(record.from);
|
|
1305
|
+
if (source) wish.from = { url: linkOf(origin, source), label: source.owner && source.app ? `${source.owner}/${source.app}` : source.name };
|
|
1306
|
+
} catch {}
|
|
1307
|
+
}
|
|
1308
|
+
return wish;
|
|
1294
1309
|
}
|
|
1295
1310
|
|
|
1296
1311
|
/** Takes a shared wish down without its secret: for the Lamp team, when a link shows what it mustn't. */
|