@lampdotdev/lamp 0.35.0 → 0.36.2-nightly.17
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
|
@@ -113,10 +113,11 @@
|
|
|
113
113
|
image: function (name) { return loadImage(LampAssets.url(name)); }
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
-
function Failure(message, needsSetup) {
|
|
116
|
+
function Failure(message, needsSetup, help) {
|
|
117
117
|
this.name = 'LampAI.Failure';
|
|
118
118
|
this.message = message;
|
|
119
119
|
this.needsSetup = needsSetup || null;
|
|
120
|
+
this.help = help || null;
|
|
120
121
|
this.stack = (new Error(message)).stack;
|
|
121
122
|
}
|
|
122
123
|
Failure.prototype = Object.create(Error.prototype);
|
|
@@ -190,6 +191,8 @@
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
// Why a copy Lamp didn't put on a device has no AI: a page on the web (its published page) is away from Lamp.
|
|
194
|
+
// Where someone with no Lamp finds how to turn AI on, for a page away from Lamp.
|
|
195
|
+
var AI_HELP = 'https://lamp.dev';
|
|
193
196
|
function unplaced() {
|
|
194
197
|
return here.protocol === 'https:'
|
|
195
198
|
? 'This page is on the web, away from Lamp, so it has no AI here. Open it in Lamp for that.'
|
|
@@ -206,7 +209,7 @@
|
|
|
206
209
|
var ipv4 = /^http:\/\/\d+\.\d+\.\d+\.\d+:/;
|
|
207
210
|
list = list.filter(function (r) { return ipv4.test(r); }).concat(list.filter(function (r) { return !ipv4.test(r); }));
|
|
208
211
|
}
|
|
209
|
-
if (!list.length) return Promise.reject(new Failure(unplaced()));
|
|
212
|
+
if (!list.length) return Promise.reject(new Failure(unplaced(), null, AI_HELP));
|
|
210
213
|
// With a deadline (LampAI.act's turn), each try gets only the time left, and none starts after it.
|
|
211
214
|
function left() { return deadline ? deadline - Date.now() : timeout; }
|
|
212
215
|
function attempt(i, round) {
|
|
@@ -229,7 +232,7 @@
|
|
|
229
232
|
function token() {
|
|
230
233
|
var kept = read('install.token', null);
|
|
231
234
|
if (kept && installed && kept.app === installed.app) return Promise.resolve(kept.token);
|
|
232
|
-
if (!installed) return Promise.reject(new Failure(unplaced()));
|
|
235
|
+
if (!installed) return Promise.reject(new Failure(unplaced(), null, AI_HELP));
|
|
233
236
|
return send('/app/claim', { app: installed.app }, null, 10000).then(function (answer) {
|
|
234
237
|
if (answer.status !== 200 || !answer.reply.token) {
|
|
235
238
|
throw new Failure('This app needs Lamp to put it on this device once more before it can use AI: press Play in Lamp on your Mac.');
|
|
@@ -240,10 +243,50 @@
|
|
|
240
243
|
});
|
|
241
244
|
}
|
|
242
245
|
|
|
246
|
+
// A page shared on lamp.dev plays from a host of its own under play.lamp.dev, away from Lamp. The person's own
|
|
247
|
+
// Lamp or lamp on this computer can answer it (Lamp's `SharedPagesAI`, or `lamp ai`, at a port of the computer's
|
|
248
|
+
// own), once they've allowed this
|
|
249
|
+
// page there; the token it's given is kept with the page's saved things. Whether there's an AI is asked first,
|
|
250
|
+
// without one, so the person is asked only when the page uses it.
|
|
251
|
+
var NEARBY = 'http://127.0.0.1:47423';
|
|
252
|
+
function shared() { return !installed && here.protocol === 'https:' && /\.play\.lamp\.dev$/.test(here.hostname || ''); }
|
|
253
|
+
function safari() {
|
|
254
|
+
var ua = (window.navigator && navigator.userAgent) || '';
|
|
255
|
+
return /Safari\//.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|Firefox|FxiOS/.test(ua);
|
|
256
|
+
}
|
|
257
|
+
function nearby(path, body, timeout, deadline) {
|
|
258
|
+
var text = JSON.stringify(body);
|
|
259
|
+
function go(t) { return exchange(NEARBY, path, t, text, deadline ? Math.max(1000, deadline - Date.now()) : timeout); }
|
|
260
|
+
function allow() {
|
|
261
|
+
return exchange(NEARBY, '/pair', null, JSON.stringify({ name: document.title || '' }), 130000).then(function (answer) {
|
|
262
|
+
if (answer.status !== 200 || !answer.reply.token) throw new Failure('This computer didn\'t allow this page to use its AI.');
|
|
263
|
+
write('nearby.token', answer.reply.token);
|
|
264
|
+
return answer.reply.token;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
var kept = read('nearby.token', null);
|
|
268
|
+
var done = path === '/ai/status' || (body && body.probe === true) ? go(kept) : (kept ? Promise.resolve(kept) : allow()).then(function (t) {
|
|
269
|
+
return go(t).then(function (answer) {
|
|
270
|
+
if (answer.status !== 401) return answer;
|
|
271
|
+
// Taken back on the computer: ask once more.
|
|
272
|
+
forget('nearby.token');
|
|
273
|
+
return allow().then(go);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
return done.then(null, function (e) {
|
|
277
|
+
if (e instanceof Failure) throw e;
|
|
278
|
+
// 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('AI here works in Chrome or in Lamp, or with Lamp\'s command line: lamp play and this page\'s link.', null, AI_HELP);
|
|
280
|
+
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
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
243
284
|
function post(path, body, timeout, deadline) {
|
|
244
285
|
timeout = timeout || 240000;
|
|
245
286
|
var done;
|
|
246
|
-
if (bridge) {
|
|
287
|
+
if (!bridge && shared()) {
|
|
288
|
+
done = nearby(path, body, timeout, deadline);
|
|
289
|
+
} else if (bridge) {
|
|
247
290
|
// The TV preview on the Mac answers for Lamp itself.
|
|
248
291
|
done = bridge.postMessage({ path: path, body: JSON.stringify(body), timeout: timeout }).then(function (r) {
|
|
249
292
|
var reply = {};
|
|
@@ -454,8 +497,11 @@
|
|
|
454
497
|
|
|
455
498
|
// Doing things for the person: LampAI.act
|
|
456
499
|
|
|
457
|
-
//
|
|
458
|
-
|
|
500
|
+
// A turn has no time limit of its own: a real answer is never cut off, however long the model takes. Each step is
|
|
501
|
+
// given ACT_STEP_SECONDS, which only a call that has stopped answering reaches. An app that wants a quicker turn
|
|
502
|
+
// asks for one (`limits: { seconds }`); steps and writes it may only lower.
|
|
503
|
+
var ACT_LIMITS = { steps: 4, writes: 6 };
|
|
504
|
+
var ACT_STEP_SECONDS = 600;
|
|
459
505
|
var actRan = {}; // turn id -> call key -> the result a write gave, while this copy of the page is open
|
|
460
506
|
function actId() { return 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
|
|
461
507
|
function isList(v) { return Object.prototype.toString.call(v) === '[object Array]'; }
|
|
@@ -481,10 +527,11 @@
|
|
|
481
527
|
function act(request, options) {
|
|
482
528
|
var o = options || {}, tools = o.tools || {};
|
|
483
529
|
var limits = {};
|
|
484
|
-
['steps', 'writes'
|
|
530
|
+
['steps', 'writes'].forEach(function (k) {
|
|
485
531
|
var asked = o.limits && typeof o.limits[k] === 'number' ? o.limits[k] : ACT_LIMITS[k];
|
|
486
532
|
limits[k] = Math.max(1, Math.min(asked, ACT_LIMITS[k]));
|
|
487
533
|
});
|
|
534
|
+
var asked = o.limits && typeof o.limits.seconds === 'number' && o.limits.seconds > 0 ? o.limits.seconds : null;
|
|
488
535
|
var id = o.turn || actId();
|
|
489
536
|
if (!actRan[id]) {
|
|
490
537
|
var kept = Object.keys(actRan);
|
|
@@ -492,7 +539,7 @@
|
|
|
492
539
|
actRan[id] = {};
|
|
493
540
|
}
|
|
494
541
|
var ran = actRan[id];
|
|
495
|
-
var started = Date.now(), deadline = started +
|
|
542
|
+
var started = Date.now(), deadline = asked ? started + Math.max(1, asked) * 1000 : Infinity;
|
|
496
543
|
var out = { turn: id, reply: '', ask: false, choices: [], done: [], failed: [], skipped: [], reads: [], stopped: null, error: '' };
|
|
497
544
|
var steps = [], route = null, writes = 0, over = false, settle;
|
|
498
545
|
var declared = Object.keys(tools).map(function (name) {
|
|
@@ -514,7 +561,7 @@
|
|
|
514
561
|
return over;
|
|
515
562
|
}
|
|
516
563
|
var result = new Promise(function (resolve) { settle = resolve; });
|
|
517
|
-
var timer = setTimeout(function () { stop('time'); },
|
|
564
|
+
var timer = asked ? setTimeout(function () { stop('time'); }, Math.max(1, asked) * 1000) : null;
|
|
518
565
|
result.then(function () { clearTimeout(timer); });
|
|
519
566
|
|
|
520
567
|
// Reads run together; then the writes, one after another, each checked right before it runs. A write that
|
|
@@ -590,23 +637,15 @@
|
|
|
590
637
|
if (over) return;
|
|
591
638
|
var left = deadline - Date.now();
|
|
592
639
|
if (left <= 500) return stop('time');
|
|
640
|
+
// The step's own time: what's left of an app's turn, or the step's guard against a call that has stopped.
|
|
641
|
+
var stepTime = Math.min(left, ACT_STEP_SECONDS * 1000);
|
|
593
642
|
var body = { request: String(request), about: o.about || '', now: present(), tools: declared, steps: steps,
|
|
594
|
-
history: (o.history || []).slice(-3), seconds: Math.floor(
|
|
643
|
+
history: (o.history || []).slice(-3), seconds: Math.floor(stepTime / 1000) };
|
|
595
644
|
if (o.context !== undefined) body.context = jsonState(o.context);
|
|
596
645
|
if (route) body.route = route;
|
|
597
646
|
if (steps.length >= limits.steps - 1) body.final = true;
|
|
598
647
|
if (o.model) body.model = o.model;
|
|
599
|
-
post('/ai/act', body,
|
|
600
|
-
// The route's own time for a turn (25 seconds over OpenRouter, a minute through Claude Code, Codex or a model
|
|
601
|
-
// on the Mac) shortens the turn's deadline, counted from its start; it never lengthens it.
|
|
602
|
-
if (!over && typeof answer.seconds === 'number' && answer.seconds > 0) {
|
|
603
|
-
var end = started + Math.min(answer.seconds, limits.seconds) * 1000;
|
|
604
|
-
if (end < deadline) {
|
|
605
|
-
deadline = end;
|
|
606
|
-
clearTimeout(timer);
|
|
607
|
-
timer = setTimeout(function () { stop('time'); }, Math.max(0, deadline - Date.now()));
|
|
608
|
-
}
|
|
609
|
-
}
|
|
648
|
+
post('/ai/act', body, stepTime, Date.now() + stepTime).then(function (answer) {
|
|
610
649
|
// Cancelled, out of time or already over: an answer that comes late runs nothing.
|
|
611
650
|
if (late()) return;
|
|
612
651
|
route = answer.route || route;
|
|
@@ -693,24 +732,42 @@
|
|
|
693
732
|
* Text calls take a few seconds; decisions well under a second; image calls 10-60 seconds. Show `error.message` on
|
|
694
733
|
* failure. Aspect ratios look like "1:1", "16:9", "3:4".
|
|
695
734
|
*
|
|
696
|
-
* Not every Mac has every kind of AI set up,
|
|
697
|
-
* `LampAI.
|
|
698
|
-
*
|
|
699
|
-
*
|
|
735
|
+
* Not every Mac has every kind of AI set up, a device away from the Mac's Wi-Fi or with Lamp closed has none, and a
|
|
736
|
+
* page shared on lamp.dev has it only on a computer that answers it. `LampAI.status('pictures')` (or 'text',
|
|
737
|
+
* 'decisions', 'act', 'hear') says whether it can answer now and, when it can't, the line to show: check it where
|
|
738
|
+
* a feature depends on it, and show the feature with its `reason` as it's given rather than words of the app's
|
|
739
|
+
* 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
|
+
* reason: "How to turn on AI". A call that fails for that reason has `needsSetup`, or `help`, set on its error.
|
|
700
741
|
*/
|
|
701
742
|
var LampAI = {
|
|
702
743
|
Failure: Failure,
|
|
703
744
|
|
|
704
|
-
/** Whether this kind of AI ('text', 'decisions', 'pictures' or '
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
745
|
+
/** 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, and `help`, when
|
|
747
|
+
* it's there, the page that says how to turn AI on, to link to beside it. */
|
|
748
|
+
status: function (capability) {
|
|
749
|
+
var no = function (reason, help) {
|
|
750
|
+
var said = { available: false, reason: reason || 'AI isn\'t set up for this here.' };
|
|
751
|
+
if (help) said.help = help;
|
|
752
|
+
return said;
|
|
753
|
+
};
|
|
754
|
+
// 'act' is asked of /ai/act itself: available only when this lamp.js has it, the app around the page lets the
|
|
755
|
+
// route through (an older one answers Not found) and the AI that answers the app can take it. 'hear' the same.
|
|
756
|
+
if (capability === 'act' || capability === 'hear') {
|
|
757
|
+
return post('/ai/' + capability, { probe: true }, 10000).then(function (reply) {
|
|
758
|
+
return reply[capability] === true ? { available: true, reason: '' } : no(reply.reason || reply.why);
|
|
759
|
+
}, function (e) { return no(e && e.message, e && e.help); });
|
|
760
|
+
}
|
|
761
|
+
return post('/ai/status', {}, 10000).then(function (reply) {
|
|
762
|
+
if (reply[capability] === true) return { available: true, reason: '' };
|
|
763
|
+
var detail = reply.details && reply.details[capability];
|
|
764
|
+
return no(detail && detail.reason);
|
|
765
|
+
}, function (e) { return no(e && e.message, e && e.help); });
|
|
712
766
|
},
|
|
713
767
|
|
|
768
|
+
/** Whether this kind of AI can answer now, as `status` says. */
|
|
769
|
+
isAvailable: function (capability) { return LampAI.status(capability).then(function (s) { return s.available; }); },
|
|
770
|
+
|
|
714
771
|
/** options: data (text from outside the app, for the AI to work on, not obey), images (pictures to look at),
|
|
715
772
|
* system, json (true asks for JSON and gives back the parsed value: describe its exact shape in the prompt),
|
|
716
773
|
* model (Lamp's id: "sonnet", "opus", "haiku", "claude-fable-5-2"). */
|
|
@@ -719,7 +776,7 @@
|
|
|
719
776
|
/**
|
|
720
777
|
* Does what the person asks, in their own words, with the app's own reads and writes: the model picks calls,
|
|
721
778
|
* the page runs them, the model sees what happened, until it replies (at most 4 model calls and 6 writes a turn,
|
|
722
|
-
*
|
|
779
|
+
* with no time limit unless the app sets one with `limits: { seconds }`; Cancel ends it at once). Check `LampAI.isAvailable('act')` first; keep the app's buttons for everything.
|
|
723
780
|
*
|
|
724
781
|
* var turn = LampAI.act('Add milk', {
|
|
725
782
|
* about: 'A family dashboard: the calendar, chores and the shopping list.',
|
|
@@ -816,6 +873,7 @@
|
|
|
816
873
|
* probability, choice, score and confidence. Fails only when no item could be judged.
|
|
817
874
|
*/
|
|
818
875
|
decideEach: function (items, questions, options) {
|
|
876
|
+
var failed = null;
|
|
819
877
|
var o = options || {}, about = o.about || function (item) { return item; };
|
|
820
878
|
var limit = Math.max(1, o.concurrency || 12), total = items.length, done = 0, next = 0;
|
|
821
879
|
var results = new Array(total), saved = o.remember ? read('verdicts', {}) : null, now = Date.now();
|
|
@@ -843,7 +901,7 @@
|
|
|
843
901
|
for (var k in (reply.answers || {})) if (reply.answers.hasOwnProperty(k)) answers[k] = decision(reply.answers[k]);
|
|
844
902
|
if (key) saved[key] = { answers: reply.answers, until: now + o.remember * 1000 };
|
|
845
903
|
finish(i, answers);
|
|
846
|
-
}, function (error) { finish(i, null, error.message); });
|
|
904
|
+
}, function (error) { if (!failed) failed = error; finish(i, null, error.message); });
|
|
847
905
|
}
|
|
848
906
|
function worker() {
|
|
849
907
|
if (next >= total) return Promise.resolve();
|
|
@@ -859,7 +917,8 @@
|
|
|
859
917
|
keys.slice(-2000).forEach(function (k) { kept[k] = saved[k]; });
|
|
860
918
|
write('verdicts', kept);
|
|
861
919
|
}
|
|
862
|
-
|
|
920
|
+
// Every item failed: the first failure's setup and help go with it, as a single decision's would.
|
|
921
|
+
if (total && results.every(function (r) { return r.error; })) throw new Failure(results[0].error, failed && failed.needsSetup, failed && failed.help);
|
|
863
922
|
return results;
|
|
864
923
|
});
|
|
865
924
|
},
|
package/cli/lamps/lamp.js
CHANGED
|
@@ -113,10 +113,11 @@
|
|
|
113
113
|
image: function (name) { return loadImage(LampAssets.url(name)); }
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
-
function Failure(message, needsSetup) {
|
|
116
|
+
function Failure(message, needsSetup, help) {
|
|
117
117
|
this.name = 'LampAI.Failure';
|
|
118
118
|
this.message = message;
|
|
119
119
|
this.needsSetup = needsSetup || null;
|
|
120
|
+
this.help = help || null;
|
|
120
121
|
this.stack = (new Error(message)).stack;
|
|
121
122
|
}
|
|
122
123
|
Failure.prototype = Object.create(Error.prototype);
|
|
@@ -190,6 +191,8 @@
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
// Why a copy Lamp didn't put on a device has no AI: a page on the web (its published page) is away from Lamp.
|
|
194
|
+
// Where someone with no Lamp finds how to turn AI on, for a page away from Lamp.
|
|
195
|
+
var AI_HELP = 'https://lamp.dev';
|
|
193
196
|
function unplaced() {
|
|
194
197
|
return here.protocol === 'https:'
|
|
195
198
|
? 'This page is on the web, away from Lamp, so it has no AI here. Open it in Lamp for that.'
|
|
@@ -206,7 +209,7 @@
|
|
|
206
209
|
var ipv4 = /^http:\/\/\d+\.\d+\.\d+\.\d+:/;
|
|
207
210
|
list = list.filter(function (r) { return ipv4.test(r); }).concat(list.filter(function (r) { return !ipv4.test(r); }));
|
|
208
211
|
}
|
|
209
|
-
if (!list.length) return Promise.reject(new Failure(unplaced()));
|
|
212
|
+
if (!list.length) return Promise.reject(new Failure(unplaced(), null, AI_HELP));
|
|
210
213
|
// With a deadline (LampAI.act's turn), each try gets only the time left, and none starts after it.
|
|
211
214
|
function left() { return deadline ? deadline - Date.now() : timeout; }
|
|
212
215
|
function attempt(i, round) {
|
|
@@ -229,7 +232,7 @@
|
|
|
229
232
|
function token() {
|
|
230
233
|
var kept = read('install.token', null);
|
|
231
234
|
if (kept && installed && kept.app === installed.app) return Promise.resolve(kept.token);
|
|
232
|
-
if (!installed) return Promise.reject(new Failure(unplaced()));
|
|
235
|
+
if (!installed) return Promise.reject(new Failure(unplaced(), null, AI_HELP));
|
|
233
236
|
return send('/app/claim', { app: installed.app }, null, 10000).then(function (answer) {
|
|
234
237
|
if (answer.status !== 200 || !answer.reply.token) {
|
|
235
238
|
throw new Failure('This app needs Lamp to put it on this device once more before it can use AI: press Play in Lamp on your Mac.');
|
|
@@ -240,10 +243,50 @@
|
|
|
240
243
|
});
|
|
241
244
|
}
|
|
242
245
|
|
|
246
|
+
// A page shared on lamp.dev plays from a host of its own under play.lamp.dev, away from Lamp. The person's own
|
|
247
|
+
// Lamp or lamp on this computer can answer it (Lamp's `SharedPagesAI`, or `lamp ai`, at a port of the computer's
|
|
248
|
+
// own), once they've allowed this
|
|
249
|
+
// page there; the token it's given is kept with the page's saved things. Whether there's an AI is asked first,
|
|
250
|
+
// without one, so the person is asked only when the page uses it.
|
|
251
|
+
var NEARBY = 'http://127.0.0.1:47423';
|
|
252
|
+
function shared() { return !installed && here.protocol === 'https:' && /\.play\.lamp\.dev$/.test(here.hostname || ''); }
|
|
253
|
+
function safari() {
|
|
254
|
+
var ua = (window.navigator && navigator.userAgent) || '';
|
|
255
|
+
return /Safari\//.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|Firefox|FxiOS/.test(ua);
|
|
256
|
+
}
|
|
257
|
+
function nearby(path, body, timeout, deadline) {
|
|
258
|
+
var text = JSON.stringify(body);
|
|
259
|
+
function go(t) { return exchange(NEARBY, path, t, text, deadline ? Math.max(1000, deadline - Date.now()) : timeout); }
|
|
260
|
+
function allow() {
|
|
261
|
+
return exchange(NEARBY, '/pair', null, JSON.stringify({ name: document.title || '' }), 130000).then(function (answer) {
|
|
262
|
+
if (answer.status !== 200 || !answer.reply.token) throw new Failure('This computer didn\'t allow this page to use its AI.');
|
|
263
|
+
write('nearby.token', answer.reply.token);
|
|
264
|
+
return answer.reply.token;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
var kept = read('nearby.token', null);
|
|
268
|
+
var done = path === '/ai/status' || (body && body.probe === true) ? go(kept) : (kept ? Promise.resolve(kept) : allow()).then(function (t) {
|
|
269
|
+
return go(t).then(function (answer) {
|
|
270
|
+
if (answer.status !== 401) return answer;
|
|
271
|
+
// Taken back on the computer: ask once more.
|
|
272
|
+
forget('nearby.token');
|
|
273
|
+
return allow().then(go);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
return done.then(null, function (e) {
|
|
277
|
+
if (e instanceof Failure) throw e;
|
|
278
|
+
// 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('AI here works in Chrome or in Lamp, or with Lamp\'s command line: lamp play and this page\'s link.', null, AI_HELP);
|
|
280
|
+
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
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
243
284
|
function post(path, body, timeout, deadline) {
|
|
244
285
|
timeout = timeout || 240000;
|
|
245
286
|
var done;
|
|
246
|
-
if (bridge) {
|
|
287
|
+
if (!bridge && shared()) {
|
|
288
|
+
done = nearby(path, body, timeout, deadline);
|
|
289
|
+
} else if (bridge) {
|
|
247
290
|
// The TV preview on the Mac answers for Lamp itself.
|
|
248
291
|
done = bridge.postMessage({ path: path, body: JSON.stringify(body), timeout: timeout }).then(function (r) {
|
|
249
292
|
var reply = {};
|
|
@@ -454,8 +497,11 @@
|
|
|
454
497
|
|
|
455
498
|
// Doing things for the person: LampAI.act
|
|
456
499
|
|
|
457
|
-
//
|
|
458
|
-
|
|
500
|
+
// A turn has no time limit of its own: a real answer is never cut off, however long the model takes. Each step is
|
|
501
|
+
// given ACT_STEP_SECONDS, which only a call that has stopped answering reaches. An app that wants a quicker turn
|
|
502
|
+
// asks for one (`limits: { seconds }`); steps and writes it may only lower.
|
|
503
|
+
var ACT_LIMITS = { steps: 4, writes: 6 };
|
|
504
|
+
var ACT_STEP_SECONDS = 600;
|
|
459
505
|
var actRan = {}; // turn id -> call key -> the result a write gave, while this copy of the page is open
|
|
460
506
|
function actId() { return 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }
|
|
461
507
|
function isList(v) { return Object.prototype.toString.call(v) === '[object Array]'; }
|
|
@@ -481,10 +527,11 @@
|
|
|
481
527
|
function act(request, options) {
|
|
482
528
|
var o = options || {}, tools = o.tools || {};
|
|
483
529
|
var limits = {};
|
|
484
|
-
['steps', 'writes'
|
|
530
|
+
['steps', 'writes'].forEach(function (k) {
|
|
485
531
|
var asked = o.limits && typeof o.limits[k] === 'number' ? o.limits[k] : ACT_LIMITS[k];
|
|
486
532
|
limits[k] = Math.max(1, Math.min(asked, ACT_LIMITS[k]));
|
|
487
533
|
});
|
|
534
|
+
var asked = o.limits && typeof o.limits.seconds === 'number' && o.limits.seconds > 0 ? o.limits.seconds : null;
|
|
488
535
|
var id = o.turn || actId();
|
|
489
536
|
if (!actRan[id]) {
|
|
490
537
|
var kept = Object.keys(actRan);
|
|
@@ -492,7 +539,7 @@
|
|
|
492
539
|
actRan[id] = {};
|
|
493
540
|
}
|
|
494
541
|
var ran = actRan[id];
|
|
495
|
-
var started = Date.now(), deadline = started +
|
|
542
|
+
var started = Date.now(), deadline = asked ? started + Math.max(1, asked) * 1000 : Infinity;
|
|
496
543
|
var out = { turn: id, reply: '', ask: false, choices: [], done: [], failed: [], skipped: [], reads: [], stopped: null, error: '' };
|
|
497
544
|
var steps = [], route = null, writes = 0, over = false, settle;
|
|
498
545
|
var declared = Object.keys(tools).map(function (name) {
|
|
@@ -514,7 +561,7 @@
|
|
|
514
561
|
return over;
|
|
515
562
|
}
|
|
516
563
|
var result = new Promise(function (resolve) { settle = resolve; });
|
|
517
|
-
var timer = setTimeout(function () { stop('time'); },
|
|
564
|
+
var timer = asked ? setTimeout(function () { stop('time'); }, Math.max(1, asked) * 1000) : null;
|
|
518
565
|
result.then(function () { clearTimeout(timer); });
|
|
519
566
|
|
|
520
567
|
// Reads run together; then the writes, one after another, each checked right before it runs. A write that
|
|
@@ -590,23 +637,15 @@
|
|
|
590
637
|
if (over) return;
|
|
591
638
|
var left = deadline - Date.now();
|
|
592
639
|
if (left <= 500) return stop('time');
|
|
640
|
+
// The step's own time: what's left of an app's turn, or the step's guard against a call that has stopped.
|
|
641
|
+
var stepTime = Math.min(left, ACT_STEP_SECONDS * 1000);
|
|
593
642
|
var body = { request: String(request), about: o.about || '', now: present(), tools: declared, steps: steps,
|
|
594
|
-
history: (o.history || []).slice(-3), seconds: Math.floor(
|
|
643
|
+
history: (o.history || []).slice(-3), seconds: Math.floor(stepTime / 1000) };
|
|
595
644
|
if (o.context !== undefined) body.context = jsonState(o.context);
|
|
596
645
|
if (route) body.route = route;
|
|
597
646
|
if (steps.length >= limits.steps - 1) body.final = true;
|
|
598
647
|
if (o.model) body.model = o.model;
|
|
599
|
-
post('/ai/act', body,
|
|
600
|
-
// The route's own time for a turn (25 seconds over OpenRouter, a minute through Claude Code, Codex or a model
|
|
601
|
-
// on the Mac) shortens the turn's deadline, counted from its start; it never lengthens it.
|
|
602
|
-
if (!over && typeof answer.seconds === 'number' && answer.seconds > 0) {
|
|
603
|
-
var end = started + Math.min(answer.seconds, limits.seconds) * 1000;
|
|
604
|
-
if (end < deadline) {
|
|
605
|
-
deadline = end;
|
|
606
|
-
clearTimeout(timer);
|
|
607
|
-
timer = setTimeout(function () { stop('time'); }, Math.max(0, deadline - Date.now()));
|
|
608
|
-
}
|
|
609
|
-
}
|
|
648
|
+
post('/ai/act', body, stepTime, Date.now() + stepTime).then(function (answer) {
|
|
610
649
|
// Cancelled, out of time or already over: an answer that comes late runs nothing.
|
|
611
650
|
if (late()) return;
|
|
612
651
|
route = answer.route || route;
|
|
@@ -693,24 +732,42 @@
|
|
|
693
732
|
* Text calls take a few seconds; decisions well under a second; image calls 10-60 seconds. Show `error.message` on
|
|
694
733
|
* failure. Aspect ratios look like "1:1", "16:9", "3:4".
|
|
695
734
|
*
|
|
696
|
-
* Not every Mac has every kind of AI set up,
|
|
697
|
-
* `LampAI.
|
|
698
|
-
*
|
|
699
|
-
*
|
|
735
|
+
* Not every Mac has every kind of AI set up, a device away from the Mac's Wi-Fi or with Lamp closed has none, and a
|
|
736
|
+
* page shared on lamp.dev has it only on a computer that answers it. `LampAI.status('pictures')` (or 'text',
|
|
737
|
+
* 'decisions', 'act', 'hear') says whether it can answer now and, when it can't, the line to show: check it where
|
|
738
|
+
* a feature depends on it, and show the feature with its `reason` as it's given rather than words of the app's
|
|
739
|
+
* 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
|
+
* reason: "How to turn on AI". A call that fails for that reason has `needsSetup`, or `help`, set on its error.
|
|
700
741
|
*/
|
|
701
742
|
var LampAI = {
|
|
702
743
|
Failure: Failure,
|
|
703
744
|
|
|
704
|
-
/** Whether this kind of AI ('text', 'decisions', 'pictures' or '
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
745
|
+
/** 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, and `help`, when
|
|
747
|
+
* it's there, the page that says how to turn AI on, to link to beside it. */
|
|
748
|
+
status: function (capability) {
|
|
749
|
+
var no = function (reason, help) {
|
|
750
|
+
var said = { available: false, reason: reason || 'AI isn\'t set up for this here.' };
|
|
751
|
+
if (help) said.help = help;
|
|
752
|
+
return said;
|
|
753
|
+
};
|
|
754
|
+
// 'act' is asked of /ai/act itself: available only when this lamp.js has it, the app around the page lets the
|
|
755
|
+
// route through (an older one answers Not found) and the AI that answers the app can take it. 'hear' the same.
|
|
756
|
+
if (capability === 'act' || capability === 'hear') {
|
|
757
|
+
return post('/ai/' + capability, { probe: true }, 10000).then(function (reply) {
|
|
758
|
+
return reply[capability] === true ? { available: true, reason: '' } : no(reply.reason || reply.why);
|
|
759
|
+
}, function (e) { return no(e && e.message, e && e.help); });
|
|
760
|
+
}
|
|
761
|
+
return post('/ai/status', {}, 10000).then(function (reply) {
|
|
762
|
+
if (reply[capability] === true) return { available: true, reason: '' };
|
|
763
|
+
var detail = reply.details && reply.details[capability];
|
|
764
|
+
return no(detail && detail.reason);
|
|
765
|
+
}, function (e) { return no(e && e.message, e && e.help); });
|
|
712
766
|
},
|
|
713
767
|
|
|
768
|
+
/** Whether this kind of AI can answer now, as `status` says. */
|
|
769
|
+
isAvailable: function (capability) { return LampAI.status(capability).then(function (s) { return s.available; }); },
|
|
770
|
+
|
|
714
771
|
/** options: data (text from outside the app, for the AI to work on, not obey), images (pictures to look at),
|
|
715
772
|
* system, json (true asks for JSON and gives back the parsed value: describe its exact shape in the prompt),
|
|
716
773
|
* model (Lamp's id: "sonnet", "opus", "haiku", "claude-fable-5-2"). */
|
|
@@ -719,7 +776,7 @@
|
|
|
719
776
|
/**
|
|
720
777
|
* Does what the person asks, in their own words, with the app's own reads and writes: the model picks calls,
|
|
721
778
|
* the page runs them, the model sees what happened, until it replies (at most 4 model calls and 6 writes a turn,
|
|
722
|
-
*
|
|
779
|
+
* with no time limit unless the app sets one with `limits: { seconds }`; Cancel ends it at once). Check `LampAI.isAvailable('act')` first; keep the app's buttons for everything.
|
|
723
780
|
*
|
|
724
781
|
* var turn = LampAI.act('Add milk', {
|
|
725
782
|
* about: 'A family dashboard: the calendar, chores and the shopping list.',
|
|
@@ -816,6 +873,7 @@
|
|
|
816
873
|
* probability, choice, score and confidence. Fails only when no item could be judged.
|
|
817
874
|
*/
|
|
818
875
|
decideEach: function (items, questions, options) {
|
|
876
|
+
var failed = null;
|
|
819
877
|
var o = options || {}, about = o.about || function (item) { return item; };
|
|
820
878
|
var limit = Math.max(1, o.concurrency || 12), total = items.length, done = 0, next = 0;
|
|
821
879
|
var results = new Array(total), saved = o.remember ? read('verdicts', {}) : null, now = Date.now();
|
|
@@ -843,7 +901,7 @@
|
|
|
843
901
|
for (var k in (reply.answers || {})) if (reply.answers.hasOwnProperty(k)) answers[k] = decision(reply.answers[k]);
|
|
844
902
|
if (key) saved[key] = { answers: reply.answers, until: now + o.remember * 1000 };
|
|
845
903
|
finish(i, answers);
|
|
846
|
-
}, function (error) { finish(i, null, error.message); });
|
|
904
|
+
}, function (error) { if (!failed) failed = error; finish(i, null, error.message); });
|
|
847
905
|
}
|
|
848
906
|
function worker() {
|
|
849
907
|
if (next >= total) return Promise.resolve();
|
|
@@ -859,7 +917,8 @@
|
|
|
859
917
|
keys.slice(-2000).forEach(function (k) { kept[k] = saved[k]; });
|
|
860
918
|
write('verdicts', kept);
|
|
861
919
|
}
|
|
862
|
-
|
|
920
|
+
// Every item failed: the first failure's setup and help go with it, as a single decision's would.
|
|
921
|
+
if (total && results.every(function (r) { return r.error; })) throw new Failure(results[0].error, failed && failed.needsSetup, failed && failed.help);
|
|
863
922
|
return results;
|
|
864
923
|
});
|
|
865
924
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"exe":"ZqxAppName","name":"Zqx App Name","
|
|
1
|
+
{"exe":"ZqxAppName","platform":"mac","name":"Zqx App Name","instance":"1DF88C4D-5D7E-40DA-8372-9DF474A63D4E"}
|