atris 3.74.9 → 3.74.10
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/commands/auth.js +4 -3
- package/commands/computer.js +9 -7
- package/commands/context-sync.js +2 -1
- package/commands/integrations.js +2 -1
- package/commands/member.js +3 -3
- package/commands/ninety-nine.js +90 -30
- package/commands/orb.js +1 -1
- package/commands/pull.js +3 -3
- package/commands/push.js +2 -2
- package/commands/skill.js +5 -0
- package/commands/soul.js +2 -1
- package/commands/stream.js +3 -2
- package/commands/strings.js +1 -1
- package/commands/unknowns.js +1 -1
- package/commands/workflow.js +1 -1
- package/commands/zen.js +4 -3
- package/lib/auto-accept-certified.js +2 -3
- package/lib/autoland.js +4 -3
- package/lib/campaign-loop.js +12 -4
- package/lib/card.js +1 -1
- package/lib/chat-log-scan.js +1 -1
- package/lib/first-minute.js +1 -0
- package/lib/gm-bet.js +51 -48
- package/lib/gm-card.js +46 -19
- package/lib/gm-phone-icon.js +80 -0
- package/lib/gm-phone-page.js +803 -96
- package/lib/gm-phone.js +15 -4
- package/lib/journal.js +1 -1
- package/lib/noninteractive.js +5 -1
- package/lib/roster-swap.js +8 -3
- package/lib/task-decision.js +1 -1
- package/lib/zen-apply.js +14 -1
- package/lib/zen.js +37 -3
- package/package.json +1 -1
- package/utils/auth.js +69 -7
package/lib/gm-bet.js
CHANGED
|
@@ -2108,7 +2108,7 @@ const WRITER_ANGLES = [
|
|
|
2108
2108
|
const SWARM_SAY = {
|
|
2109
2109
|
drafting: n => (n === 1 ? 'drafting an idea' : `drafting ${COUNT_WORDS[n] || n} ideas at once`),
|
|
2110
2110
|
checking: k => `the ${ORDINALS[k - 1] || 'next'} draft is in. checking it`,
|
|
2111
|
-
redraft: 'one draft
|
|
2111
|
+
redraft: 'one draft did not fit. redrafting it',
|
|
2112
2112
|
shorten: 'one draft ran long. shortening it',
|
|
2113
2113
|
heldUp: 'found one that holds up',
|
|
2114
2114
|
notHeld: 'that one did not hold up. still working',
|
|
@@ -3289,10 +3289,10 @@ function createBetDesk(deps) {
|
|
|
3289
3289
|
// the number for the card on screen, saved beside the ledger
|
|
3290
3290
|
function report({ value } = {}) {
|
|
3291
3291
|
const item = onJob('report');
|
|
3292
|
-
if (!item) return { ok: false, reason: 'gone', message: 'there is no number asked for on screen' };
|
|
3292
|
+
if (!item) return { ok: false, reason: 'gone', message: 'there is no number asked for on screen.' };
|
|
3293
3293
|
const raw = String(value == null ? '' : value).replace(/,/g, '').trim();
|
|
3294
3294
|
const v = Number(raw);
|
|
3295
|
-
if (!raw || !Number.isFinite(v)) return { ok: false, reason: 'bad_number', message: 'that is not a number' };
|
|
3295
|
+
if (!raw || !Number.isFinite(v)) return { ok: false, reason: 'bad_number', message: 'that is not a number.' };
|
|
3296
3296
|
appendLedger(scoreboardFile, { workspace: deps.workspaceRoot, kind: 'report', value: v, at: iso() });
|
|
3297
3297
|
log({ card: 'bet', choice: 'report', command: null, ran: 'in process', exit_code: 0 });
|
|
3298
3298
|
state.status = 'cleared';
|
|
@@ -3387,7 +3387,7 @@ function createBetDesk(deps) {
|
|
|
3387
3387
|
function pick({ id } = {}) {
|
|
3388
3388
|
const cur = onJob('choice');
|
|
3389
3389
|
const item = cur && cur.items.find(i => i.id === id);
|
|
3390
|
-
if (!item) return { ok: false, reason: 'gone', message: 'there is nothing to pick on screen' };
|
|
3390
|
+
if (!item) return { ok: false, reason: 'gone', message: 'there is nothing to pick on screen.' };
|
|
3391
3391
|
for (const other of cur.items) if (other !== item) record(other.id, { status: 'passed', note: 'not picked' });
|
|
3392
3392
|
log({ card: 'bet', choice: 'pick', command: null, ran: 'in process', exit_code: 0, bet_id: item.id });
|
|
3393
3393
|
state.current = item;
|
|
@@ -3732,15 +3732,16 @@ function createBetDesk(deps) {
|
|
|
3732
3732
|
const isBet = () => state.status === 'ready' && state.current && (!state.current.kind || state.current.kind === 'bet');
|
|
3733
3733
|
|
|
3734
3734
|
function skip() {
|
|
3735
|
-
if (!isBet()) return { ok: false, reason: 'gone', message: 'there is no idea on screen to skip' };
|
|
3735
|
+
if (!isBet()) return { ok: false, reason: 'gone', message: 'there is no idea on screen to skip.' };
|
|
3736
3736
|
record(state.current.id, { status: 'skipped' });
|
|
3737
3737
|
log({ card: 'bet', choice: 'skip', command: null, ran: 'in process', exit_code: 0, bet_id: state.current.id });
|
|
3738
3738
|
state.status = 'skipped';
|
|
3739
|
-
|
|
3739
|
+
// the page says what to do next under the orb, so this only says what happened
|
|
3740
|
+
return { ok: true, ack: 'set aside.' };
|
|
3740
3741
|
}
|
|
3741
3742
|
|
|
3742
3743
|
async function doIt() {
|
|
3743
|
-
if (!isBet()) return { ok: false, reason: 'gone', message: 'there is no idea on screen to start' };
|
|
3744
|
+
if (!isBet()) return { ok: false, reason: 'gone', message: 'there is no idea on screen to start.' };
|
|
3744
3745
|
const { id, bet } = state.current;
|
|
3745
3746
|
const pick = pickOwner(bet, deps.workspaceRoot);
|
|
3746
3747
|
const startArgs = missionStartArgs(bet, pick, id, deps.workspaceRoot);
|
|
@@ -3766,7 +3767,7 @@ function createBetDesk(deps) {
|
|
|
3766
3767
|
let line;
|
|
3767
3768
|
// what the owner reads; the command and exit code stay in the log
|
|
3768
3769
|
if (res.code === 0 && ran.action === 'mission_run_detached') line = `${owner} has it and is starting now.`;
|
|
3769
|
-
else if (ran.reason === 'human_asks_open') line = `${owner} has it, and is waiting
|
|
3770
|
+
else if (ran.reason === 'human_asks_open') line = `${owner} has it, and is waiting for your yes before starting.`;
|
|
3770
3771
|
else line = `${owner} has it, but has not started yet. it can be started from the mac.`;
|
|
3771
3772
|
state.status = 'started';
|
|
3772
3773
|
state.mission = { id: missionId, owner, line, commands: [display(startArgs), display(runArgs)] };
|
|
@@ -3780,7 +3781,7 @@ function createBetDesk(deps) {
|
|
|
3780
3781
|
// nice: a finished job was seen; nothing else happens
|
|
3781
3782
|
function nice() {
|
|
3782
3783
|
const item = onJob('done');
|
|
3783
|
-
if (!item) return { ok: false, reason: 'gone', message: 'there is no finished job on screen' };
|
|
3784
|
+
if (!item) return { ok: false, reason: 'gone', message: 'there is no finished job on screen.' };
|
|
3784
3785
|
record(item.job.bet_id, { done_seen_at: iso() });
|
|
3785
3786
|
log({ card: 'bet', choice: 'nice', command: null, ran: 'in process', exit_code: 0, bet_id: item.job.bet_id });
|
|
3786
3787
|
state.status = 'cleared';
|
|
@@ -3793,18 +3794,18 @@ function createBetDesk(deps) {
|
|
|
3793
3794
|
// and its answer is saved against nothing (answer ids repeat across questions)
|
|
3794
3795
|
function answer({ choice, text, card } = {}) {
|
|
3795
3796
|
const item = onJob('question');
|
|
3796
|
-
if (!item) return { ok: false, reason: 'gone', message: 'there is no question on screen' };
|
|
3797
|
-
if (card !== undefined && card !== item.id) return { ok: false, reason: 'gone', message: 'that card changed, so nothing was saved.
|
|
3797
|
+
if (!item) return { ok: false, reason: 'gone', message: 'there is no question on screen.' };
|
|
3798
|
+
if (card !== undefined && card !== item.id) return { ok: false, reason: 'gone', message: 'that card changed, so nothing was saved. here is the one on screen now.' };
|
|
3798
3799
|
const picked = item.question.choices.find(c => c.id === choice);
|
|
3799
|
-
if (!picked) return { ok: false, reason: 'bad_choice', message: 'that is not one of the answers' };
|
|
3800
|
+
if (!picked) return { ok: false, reason: 'bad_choice', message: 'that is not one of the answers.' };
|
|
3800
3801
|
const q = item.question;
|
|
3801
3802
|
if (q.key === 'scoreboard') {
|
|
3802
3803
|
let name;
|
|
3803
3804
|
let up;
|
|
3804
3805
|
if (picked.other) {
|
|
3805
3806
|
name = String(text || '').trim();
|
|
3806
|
-
if (!name) return { ok: false, reason: 'empty', message: '
|
|
3807
|
-
if (name.length > 60) return { ok: false, reason: 'too_long', message: 'keep it under 60 characters' };
|
|
3807
|
+
if (!name) return { ok: false, reason: 'empty', message: 'name the number in a few words.' };
|
|
3808
|
+
if (name.length > 60) return { ok: false, reason: 'too_long', message: 'keep it under 60 characters.' };
|
|
3808
3809
|
up = !LOWER_IS_BETTER.test(name);
|
|
3809
3810
|
} else {
|
|
3810
3811
|
const pick = q.numbers[Number(picked.id.slice(1)) - 1];
|
|
@@ -3899,7 +3900,7 @@ function createBetDesk(deps) {
|
|
|
3899
3900
|
if (job.bet_id) record(job.bet_id, { yes_answered: { mission_id: job.mission_id, step: job.lock_step, at: iso() } });
|
|
3900
3901
|
clear();
|
|
3901
3902
|
pollJobs();
|
|
3902
|
-
return { ok: true, ack:
|
|
3903
|
+
return { ok: true, ack: `told ${job.owner} yes.` };
|
|
3903
3904
|
}
|
|
3904
3905
|
const listed = await deps.runCli(['decide', '--json']);
|
|
3905
3906
|
const open = (parseJsonOut(listed.stdout) || {}).decisions || [];
|
|
@@ -3913,13 +3914,13 @@ function createBetDesk(deps) {
|
|
|
3913
3914
|
const args = ['decide', String(match.number), 'yes', '--json'];
|
|
3914
3915
|
const res = await deps.runCli(args);
|
|
3915
3916
|
log({ card: 'bet', choice: 'yes', command: require('./gm-phone').displayCommand(args), exit_code: res.code, bet_id: job.bet_id, mission_id: job.mission_id });
|
|
3916
|
-
if (res.code !== 0) return { ok: false, reason: 'answer_failed', message: 'your yes did not go through
|
|
3917
|
+
if (res.code !== 0) return { ok: false, reason: 'answer_failed', message: 'your yes did not go through. try again.' };
|
|
3917
3918
|
state.seenYes.add(job.key);
|
|
3918
3919
|
state.answeredYes.add(job.key);
|
|
3919
3920
|
if (job.bet_id) record(job.bet_id, { yes_answered: { mission_id: job.mission_id, ask_index: job.ask_index, at: iso() } });
|
|
3920
3921
|
clear();
|
|
3921
3922
|
pollJobs();
|
|
3922
|
-
return { ok: true, ack:
|
|
3923
|
+
return { ok: true, ack: `told ${job.owner} yes.` };
|
|
3923
3924
|
}
|
|
3924
3925
|
|
|
3925
3926
|
// no: the one request on the card is told no, and the job goes on without
|
|
@@ -3955,7 +3956,7 @@ function createBetDesk(deps) {
|
|
|
3955
3956
|
const args = ['decide', String(match.number), 'no', '--json'];
|
|
3956
3957
|
const res = await deps.runCli(args);
|
|
3957
3958
|
log({ card: 'bet', choice: 'no', command: display(args), exit_code: res.code, bet_id: job.bet_id, mission_id: job.mission_id });
|
|
3958
|
-
if (res.code !== 0) return { ok: false, reason: 'answer_failed', message: 'your no did not go through
|
|
3959
|
+
if (res.code !== 0) return { ok: false, reason: 'answer_failed', message: 'your no did not go through. try again.' };
|
|
3959
3960
|
if (job.bet_id) record(job.bet_id, { no_answered: { mission_id: job.mission_id, ask_index: job.ask_index, at: iso() } });
|
|
3960
3961
|
return done(told);
|
|
3961
3962
|
}
|
|
@@ -3966,7 +3967,7 @@ function createBetDesk(deps) {
|
|
|
3966
3967
|
const j = item.job;
|
|
3967
3968
|
const expect = item.kind === 'yes' ? j.expect : j.bet && j.bet.expect;
|
|
3968
3969
|
const why = expect ? ` ${sentence(expect)}` : '';
|
|
3969
|
-
if (item.kind === 'yes') return { what: plainAsk(j.ask), why: `${j.owner} is waiting
|
|
3970
|
+
if (item.kind === 'yes') return { what: plainAsk(j.ask), why: `${j.owner} is waiting for your yes.${why}` };
|
|
3970
3971
|
return { what: j.title, why: `this is done.${why}` };
|
|
3971
3972
|
}
|
|
3972
3973
|
|
|
@@ -4074,7 +4075,7 @@ function createBetDesk(deps) {
|
|
|
4074
4075
|
function cardForReader(v) {
|
|
4075
4076
|
if (v.kind === 'question') return { kind: 'question', title: v.question.ask, detail: v.question.line, buttons: v.question.choices.map(c => c.label) };
|
|
4076
4077
|
if (v.kind === 'bet' && v.bet.concern) return { kind: 'idea', title: v.bet.title, detail: [v.bet.why_now, v.bet.expect, v.bet.concern].join(' '), buttons: ['go with it', 'sharpen it', 'something else'] };
|
|
4077
|
-
if (v.kind === 'bet') return { kind: 'idea', title: v.bet.title, detail: [v.bet.why_now, v.bet.expect].join(' '), buttons: ['do it', '
|
|
4078
|
+
if (v.kind === 'bet') return { kind: 'idea', title: v.bet.title, detail: [v.bet.why_now, v.bet.expect].join(' '), buttons: ['do it', 'sharpen it', 'not now'] };
|
|
4078
4079
|
if (v.kind === 'choice') return { kind: 'choice', title: 'which one next?', detail: v.choice.options.map(o => o.title).join(' / '), buttons: [] };
|
|
4079
4080
|
if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: ['save', 'not now'] };
|
|
4080
4081
|
return { kind: v.kind, title: v.job ? v.job.what : '', detail: v.job ? v.job.why : '', buttons: [] };
|
|
@@ -4108,12 +4109,12 @@ function createBetDesk(deps) {
|
|
|
4108
4109
|
// next idea fits it; on a yes or a finished job, a word to that member.
|
|
4109
4110
|
async function say({ text, confirm = false, note = false, card } = {}) {
|
|
4110
4111
|
const said = String(text || '').trim();
|
|
4111
|
-
if (!said) return { ok: false, reason: 'empty', message: 'say something first' };
|
|
4112
|
-
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters
|
|
4112
|
+
if (!said) return { ok: false, reason: 'empty', message: 'say something first.' };
|
|
4113
|
+
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters.` };
|
|
4113
4114
|
let cur = state.status === 'ready' ? state.current : null;
|
|
4114
|
-
if (!cur) return { ok: false, reason: 'gone', message: 'there is no card on screen' };
|
|
4115
|
+
if (!cur) return { ok: false, reason: 'gone', message: 'there is no card on screen.' };
|
|
4115
4116
|
// words said on a card the page no longer shows act on nothing
|
|
4116
|
-
if (card !== undefined && card !== cur.id) return { ok: false, reason: 'gone', message: 'that card changed, so nothing was done.
|
|
4117
|
+
if (card !== undefined && card !== cur.id) return { ok: false, reason: 'gone', message: 'that card changed, so nothing was done. here is the one on screen now.' };
|
|
4117
4118
|
// on the calm line there is nothing to answer: the words are a note for the idea being written, read by no model
|
|
4118
4119
|
if (cur.kind === 'wait') return noteOnWait(cur, said);
|
|
4119
4120
|
let kind = cur.kind || 'bet';
|
|
@@ -4128,7 +4129,7 @@ function createBetDesk(deps) {
|
|
|
4128
4129
|
// said on the try-again card that took its place, never dropped
|
|
4129
4130
|
const replaced = kind === 'wait' && state.status === 'ready' && state.current && state.current.kind === 'missed' && state.current.after === cur.id;
|
|
4130
4131
|
if (replaced) { cur = state.current; kind = 'missed'; }
|
|
4131
|
-
if (state.current !== cur || state.status !== 'ready') return { ok: false, reason: 'gone', message: 'that card changed, so nothing was done' };
|
|
4132
|
+
if (state.current !== cur || state.status !== 'ready') return { ok: false, reason: 'gone', message: 'that card changed, so nothing was done. here is the one on screen now.' };
|
|
4132
4133
|
if (turn.stay) return turn.stay;
|
|
4133
4134
|
read = turn.act;
|
|
4134
4135
|
}
|
|
@@ -4208,10 +4209,10 @@ function createBetDesk(deps) {
|
|
|
4208
4209
|
// answered from what the table knows; an instruction steers them.
|
|
4209
4210
|
async function talk({ member, text, confirm = false, note = false } = {}) {
|
|
4210
4211
|
const name = String(member || '');
|
|
4211
|
-
if (!(team || []).some(m => m.name === name)) return { ok: false, reason: 'gone', message: 'there is no such member on the team' };
|
|
4212
|
+
if (!(team || []).some(m => m.name === name)) return { ok: false, reason: 'gone', message: 'there is no such member on the team.' };
|
|
4212
4213
|
const said = String(text || '').trim();
|
|
4213
|
-
if (!said) return { ok: false, reason: 'empty', message: 'say something first' };
|
|
4214
|
-
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters
|
|
4214
|
+
if (!said) return { ok: false, reason: 'empty', message: 'say something first.' };
|
|
4215
|
+
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters.` };
|
|
4215
4216
|
const t = threadFor(`member:${name}`);
|
|
4216
4217
|
if (!confirm && !note) {
|
|
4217
4218
|
const row = statusTable().rows.find(r => r.member === name);
|
|
@@ -4247,13 +4248,13 @@ function createBetDesk(deps) {
|
|
|
4247
4248
|
if (!OPEN_MISSION.has(status)) return null;
|
|
4248
4249
|
const asks = (Array.isArray(m.human_asks) ? m.human_asks : []).some(a => a && !(typeof a === 'object' && a.answered_at))
|
|
4249
4250
|
|| require('./yes-lock').askedYesLocks(lockDir, m.id).length > 0;
|
|
4250
|
-
if (asks) return { moved, row: { on, status: 'waiting for your yes', time: `
|
|
4251
|
+
if (asks) return { moved, row: { on, status: 'waiting for your yes', time: `asked ${spanWords(age)} ago` } };
|
|
4251
4252
|
if (driverAlive(m) || age <= MOVING_MS) {
|
|
4252
4253
|
const wall = Number(m.max_wall_seconds) || Number(m.budget_contract && m.budget_contract.requested_seconds) || 0;
|
|
4253
4254
|
const left = wall ? created + wall * 1000 - now() : 0;
|
|
4254
4255
|
return { moved, row: { on, status: 'working', time: left > 0 ? leftWords(left) : `at it for ${spanWords(now() - created)}` } };
|
|
4255
4256
|
}
|
|
4256
|
-
if (!m.last_tick_at) return { moved, row: { on, status: 'waiting to start', time: `
|
|
4257
|
+
if (!m.last_tick_at) return { moved, row: { on, status: 'waiting to start', time: `added ${spanWords(now() - created)} ago` } };
|
|
4257
4258
|
return { moved, row: { on, status: 'stopped', time: `stopped ${spanWords(age)} ago` } };
|
|
4258
4259
|
}
|
|
4259
4260
|
|
|
@@ -4281,7 +4282,7 @@ function createBetDesk(deps) {
|
|
|
4281
4282
|
const at = Date.parse((hnd && (hnd.closed_at || hnd.created_at)) || '') || 0;
|
|
4282
4283
|
if (!hnd || !hnd.to || !at || now() - at > RECENT_MS) continue;
|
|
4283
4284
|
const on = clip(String(hnd.summary || '').replace(/^\[[^\]]*\]\s*/, ''), 100);
|
|
4284
|
-
if (hnd.status === 'open') put(hnd.to, { on, status: 'waiting to start', time: `
|
|
4285
|
+
if (hnd.status === 'open') put(hnd.to, { on, status: 'waiting to start', time: `added ${spanWords(now() - at)} ago` }, at);
|
|
4285
4286
|
else put(hnd.to, { on, status: 'idle', time: `free for ${spanWords(now() - at)}` }, at);
|
|
4286
4287
|
}
|
|
4287
4288
|
const rows = [...best.values()]
|
|
@@ -4336,7 +4337,7 @@ function createBetDesk(deps) {
|
|
|
4336
4337
|
// the owner's tap is the approval, so there is no yes card. scope member
|
|
4337
4338
|
// gives only this member the model, scope job swaps the lead of its job.
|
|
4338
4339
|
function onTeam(name) { return (team || []).some(m => m.name === name); }
|
|
4339
|
-
const NOT_ON_TEAM = { ok: false, reason: 'gone', message: 'there is no such member on the team' };
|
|
4340
|
+
const NOT_ON_TEAM = { ok: false, reason: 'gone', message: 'there is no such member on the team.' };
|
|
4340
4341
|
function engineCard({ member } = {}) {
|
|
4341
4342
|
const name = String(member || '');
|
|
4342
4343
|
if (!onTeam(name)) return NOT_ON_TEAM;
|
|
@@ -4414,7 +4415,7 @@ function createBetDesk(deps) {
|
|
|
4414
4415
|
const at = Date.parse((hnd && hnd.created_at) || '') || 0;
|
|
4415
4416
|
if (!hnd || hnd.status !== 'open' || !names.has(hnd.to) || !at || now() - at > RECENT_MS) continue;
|
|
4416
4417
|
const on = clip(String(hnd.summary || '').replace(/^\[[^\]]*\]\s*/, ''), 100);
|
|
4417
|
-
jobs.push({ id: `handoff:${hnd.id || at}`, mission: null, member: hnd.to, row: { on, status: 'waiting to start', time: `
|
|
4418
|
+
jobs.push({ id: `handoff:${hnd.id || at}`, mission: null, member: hnd.to, row: { on, status: 'waiting to start', time: `added ${spanWords(now() - at)} ago` }, moved: at });
|
|
4418
4419
|
}
|
|
4419
4420
|
jobs.sort((x, y) => STATUS_ORDER.indexOf(x.row.status) - STATUS_ORDER.indexOf(y.row.status) || y.moved - x.moved || x.member.localeCompare(y.member));
|
|
4420
4421
|
// a yes skipped with not now waits at the back of the ones that need the owner
|
|
@@ -4459,10 +4460,10 @@ function createBetDesk(deps) {
|
|
|
4459
4460
|
|
|
4460
4461
|
async function steer({ member, text, confirm = false, note = false } = {}) {
|
|
4461
4462
|
const name = String(member || '');
|
|
4462
|
-
if (!(team || []).some(m => m.name === name)) return { ok: false, reason: 'gone', message: 'there is no such member on the team' };
|
|
4463
|
+
if (!(team || []).some(m => m.name === name)) return { ok: false, reason: 'gone', message: 'there is no such member on the team.' };
|
|
4463
4464
|
const said = String(text || '').trim();
|
|
4464
|
-
if (!said) return { ok: false, reason: 'empty', message: 'say something first' };
|
|
4465
|
-
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters
|
|
4465
|
+
if (!said) return { ok: false, reason: 'empty', message: 'say something first.' };
|
|
4466
|
+
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters.` };
|
|
4466
4467
|
if (note) {
|
|
4467
4468
|
steeringNote(name, said);
|
|
4468
4469
|
log({ card: 'bet', choice: 'steer_note', command: null, ran: 'in process', exit_code: 0, member: name, chars: said.length });
|
|
@@ -4481,7 +4482,7 @@ function createBetDesk(deps) {
|
|
|
4481
4482
|
if (out.action === 'member_run_not_started' && out.reason) return { ok: false, reason: 'not_started', message: `${name} did not start: ${String(out.reason).replace(/[.\s]+$/, '')}.` };
|
|
4482
4483
|
return { ok: false, reason: 'not_started', message: `${name} did not start. try again in a minute.` };
|
|
4483
4484
|
}
|
|
4484
|
-
if (out.action === 'member_not_running' || res.code === 3) return { ok: false, reason: 'needs_confirm', message: `tap again to start ${name} on this
|
|
4485
|
+
if (out.action === 'member_not_running' || res.code === 3) return { ok: false, reason: 'needs_confirm', message: `tap again to start ${name} on this.` };
|
|
4485
4486
|
return { ok: false, reason: 'steer_failed', message: 'it did not reach them. try again in a minute.' };
|
|
4486
4487
|
}
|
|
4487
4488
|
|
|
@@ -4566,8 +4567,9 @@ function createBetDesk(deps) {
|
|
|
4566
4567
|
const at = Math.min(l.at || 0, cards.length - 1);
|
|
4567
4568
|
const c = { ...cards[at], said: (l.said && l.said[cards[at].job_id]) || null };
|
|
4568
4569
|
const r = c.rows[0];
|
|
4569
|
-
// the deck card is about the job: its row without the rating and the model, which live on the team table
|
|
4570
|
-
|
|
4570
|
+
// the deck card is about the job: its row without the rating and the model, which live on the team table,
|
|
4571
|
+
// and without done, which says "not yet" on every card because the deck holds only open jobs
|
|
4572
|
+
const columns = DECK_COLUMNS.filter(k => k !== 'ovr' && k !== 'engine' && k !== 'done');
|
|
4571
4573
|
return gmCard.deck({ id: l.id, at, total: cards.length, card: c, columns, job: { member: c.member, role: r.role, ovr: r.ovr, engine: r.engine } });
|
|
4572
4574
|
}
|
|
4573
4575
|
if (l.kind === 'swap') {
|
|
@@ -4655,7 +4657,7 @@ function createBetDesk(deps) {
|
|
|
4655
4657
|
const base = stays && under && under.kind === 'wait' && state.current && state.current.kind === 'missed' ? (underCard() || under) : under;
|
|
4656
4658
|
const back = replied(out, base.id, text);
|
|
4657
4659
|
if (back) return back;
|
|
4658
|
-
if (!out.ok) return refuse(out.reason || 'gone', out.message || 'that did not send. try again');
|
|
4660
|
+
if (!out.ok) return refuse(out.reason || 'gone', out.message || 'that did not send. try again.');
|
|
4659
4661
|
state.reply = null;
|
|
4660
4662
|
next();
|
|
4661
4663
|
return done200({ ack: out.ack });
|
|
@@ -4669,7 +4671,7 @@ function createBetDesk(deps) {
|
|
|
4669
4671
|
const out = await talk({ member: name, text, ...more });
|
|
4670
4672
|
const back = replied(out, under.id, text);
|
|
4671
4673
|
if (back) return back;
|
|
4672
|
-
if (!out.ok) return refuse(out.reason || 'gone', out.message || 'that did not send. try again');
|
|
4674
|
+
if (!out.ok) return refuse(out.reason || 'gone', out.message || 'that did not send. try again.');
|
|
4673
4675
|
state.reply = null;
|
|
4674
4676
|
if (l.kind === 'deck') { l.said = { ...(l.said || {}), [under.deck_job]: out.ack }; l.id = freshId('deck'); } else { l.line = out.ack; l.id = freshId('member'); }
|
|
4675
4677
|
return done200();
|
|
@@ -4711,18 +4713,18 @@ function createBetDesk(deps) {
|
|
|
4711
4713
|
return done200(out.busy ? { busy: true } : {});
|
|
4712
4714
|
}
|
|
4713
4715
|
if (body.open === 'team' || body.open === 'deck') {
|
|
4714
|
-
if (stale) return refuse('gone', 'that card changed. here is the one on screen now');
|
|
4716
|
+
if (stale) return refuse('gone', 'that card changed. here is the one on screen now.');
|
|
4715
4717
|
open(body.open);
|
|
4716
4718
|
return done200();
|
|
4717
4719
|
}
|
|
4718
4720
|
// an option or words always name the card they were for
|
|
4719
|
-
if (!shown || !cardId || stale) return refuse('gone', 'that card changed, so nothing was done. here is the one on screen now');
|
|
4721
|
+
if (!shown || !cardId || stale) return refuse('gone', 'that card changed, so nothing was done. here is the one on screen now.');
|
|
4720
4722
|
const optionId = body.option_id == null ? null : String(body.option_id);
|
|
4721
4723
|
const opt = optionId ? shown.options.find(o => o.id === optionId) : null;
|
|
4722
|
-
if (optionId && !opt) return refuse('bad_option', 'that is not on this card');
|
|
4723
|
-
if (!opt && !shown.input) return refuse('bad_option', 'this card has no box to type in');
|
|
4724
|
+
if (optionId && !opt) return refuse('bad_option', 'that is not on this card.');
|
|
4725
|
+
if (!opt && !shown.input) return refuse('bad_option', 'this card has no box to type in.');
|
|
4724
4726
|
const typed = String(body.text == null ? '' : body.text).trim();
|
|
4725
|
-
if ((!opt || opt.type) && !typed) return refuse('empty', 'say something first');
|
|
4727
|
+
if ((!opt || opt.type) && !typed) return refuse('empty', 'say something first.');
|
|
4726
4728
|
if (opt && opt.confirm && body.confirm !== true) return { status: 409, body: { ...envelope(), ok: false, reason: 'needs_confirm', message: opt.confirm } };
|
|
4727
4729
|
const action = opt ? opt.action : shown.input.action;
|
|
4728
4730
|
const b = (opt && opt.binds) || {};
|
|
@@ -4827,10 +4829,11 @@ function createBetDesk(deps) {
|
|
|
4827
4829
|
case 'swap_to': {
|
|
4828
4830
|
const out = swapEngine({ member: state.layer.name, engine: b.engine, model: b.model, scope: b.scope });
|
|
4829
4831
|
if (!out.ok) return refuse(out.reason, out.message);
|
|
4832
|
+
// the team comes back with the line on it, so nothing says it twice
|
|
4830
4833
|
open('team', { line: out.ack });
|
|
4831
|
-
return done200(
|
|
4834
|
+
return done200();
|
|
4832
4835
|
}
|
|
4833
|
-
default: return refuse('bad_option', 'that does nothing here');
|
|
4836
|
+
default: return refuse('bad_option', 'that does nothing here.');
|
|
4834
4837
|
}
|
|
4835
4838
|
}
|
|
4836
4839
|
|
package/lib/gm-card.js
CHANGED
|
@@ -97,7 +97,7 @@ function idea({ id, title, body, line = null, unstarted = null, concern = null }
|
|
|
97
97
|
line: line || unstarted || null,
|
|
98
98
|
options: [
|
|
99
99
|
answer(id, { id: 'do', label: 'do it', action: 'do', style: 'primary', confirm: 'tap again to start' }),
|
|
100
|
-
answer(id, { id: 'sharper', label: '
|
|
100
|
+
answer(id, { id: 'sharper', label: 'sharpen it', action: 'sharper' }),
|
|
101
101
|
answer(id, { id: 'skip', label: 'not now', action: 'skip' }),
|
|
102
102
|
],
|
|
103
103
|
input: sayInput(),
|
|
@@ -175,6 +175,11 @@ function missed({ id }) {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// the way back from a reply that asks its own question names the card it returns to
|
|
179
|
+
const IDEA_KINDS = new Set(['idea', 'started']);
|
|
180
|
+
const ASK_KINDS = new Set(['question', 'yes', 'report', 'choice']);
|
|
181
|
+
const backToBase = base => (IDEA_KINDS.has(base.kind) ? 'back to the idea' : ASK_KINDS.has(base.kind) ? 'back to the question' : 'back');
|
|
182
|
+
|
|
178
183
|
// A reply to words said on a card. When it asks its own question (it ends
|
|
179
184
|
// in a question mark) only its answers show, with the box and a quiet way
|
|
180
185
|
// back to the first question. When it only explains, the first card's own
|
|
@@ -200,7 +205,7 @@ function reply({ id, base, text, thread = [], suggestions = [], confirm = null }
|
|
|
200
205
|
...card, asks: id,
|
|
201
206
|
options: [
|
|
202
207
|
...suggestions.map((s, i) => answer(id, { id: `r${i + 1}`, label: s, action: 'say', style: 'choice', binds: { text: s } })),
|
|
203
|
-
nav({ id: 'back', label: base.kind === 'deck' ? 'back to the job' : base.kind === 'member' ? `back to ${base.title}` :
|
|
208
|
+
nav({ id: 'back', label: base.kind === 'deck' ? 'back to the job' : base.kind === 'member' ? `back to ${base.title}` : backToBase(base), action: 'back' }),
|
|
204
209
|
],
|
|
205
210
|
input: base.input || sayInput(),
|
|
206
211
|
};
|
|
@@ -235,10 +240,10 @@ function team({ id, rows, line = null, deck = false }) {
|
|
|
235
240
|
options.push(nav({ id: `talk:${r.member}`, label: r.member, action: 'member', binds: { member: r.member }, cell: { row: i, column: 'member' } }));
|
|
236
241
|
if (r.engine !== 'person') options.push(nav({ id: `swap:${r.member}`, label: r.engine, action: 'swap', binds: { member: r.member }, cell: { row: i, column: 'engine' } }));
|
|
237
242
|
});
|
|
238
|
-
if (deck) options.push(nav({ id: 'deck', label: 'the jobs
|
|
243
|
+
if (deck) options.push(nav({ id: 'deck', label: 'go through the jobs', action: 'deck' }));
|
|
239
244
|
options.push(nav({ id: 'back', label: 'close', action: 'back' }));
|
|
240
245
|
return {
|
|
241
|
-
id, kind: 'team', asks: id, title: 'the team', body: rows.length ? 'tap a name to talk to them, or
|
|
246
|
+
id, kind: 'team', asks: id, title: 'the team', body: rows.length ? 'tap a name to talk to them, or their engine to change it.' : 'nobody is on the team yet.', line,
|
|
242
247
|
table: rows.length ? { columns: TEAM_COLUMNS, rows: rows.map(r => [r.member, r.role, r.ovr, r.engine, r.sentence]), status: rows.map(r => r.status) } : null,
|
|
243
248
|
options, input: null,
|
|
244
249
|
};
|
|
@@ -253,37 +258,39 @@ function member({ id, name, sentence: said, job = null, line = null }) {
|
|
|
253
258
|
};
|
|
254
259
|
}
|
|
255
260
|
|
|
256
|
-
// what a job in the deck is doing, when there is no yes on it to answer
|
|
261
|
+
// what a job in the deck is doing, when there is no yes on it to answer;
|
|
262
|
+
// how long it has been is the card's own time line, so it is said once
|
|
257
263
|
function deckLine(card) {
|
|
258
|
-
|
|
259
|
-
if (card.label === 'working') return `${card.member} is working on it${when}. nothing needs you here.`;
|
|
264
|
+
if (card.label === 'working') return `${card.member} is working on it. nothing needs you here.`;
|
|
260
265
|
if (card.label === 'you answered') return `you answered this, so it is back with ${card.member}.`;
|
|
261
|
-
return `${card.member} has not started it yet
|
|
266
|
+
return `${card.member} has not started it yet.`;
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
// one running job: its place in the deck, the yes it waits on, its table
|
|
265
270
|
// row, and a box to talk to its member
|
|
266
271
|
function deck({ id, at, total, card, columns, job = null }) {
|
|
267
272
|
const options = [];
|
|
273
|
+
// previous and next first: the page draws them at the top, beside the card's place in the deck
|
|
274
|
+
if (total > 1) options.push(nav({ id: 'prev', label: 'previous', action: 'flip', binds: { step: -1 } }), nav({ id: 'next', label: 'next', action: 'flip', binds: { step: 1 } }));
|
|
268
275
|
if (card.yes) {
|
|
269
276
|
const binds = { job: card.mission_id, key: card.yes.key, hash: card.yes.hash || null };
|
|
270
277
|
options.push(answer(id, { id: 'yes', label: 'yes', action: 'yes', style: 'primary', confirm: 'tap again to say yes', binds }));
|
|
271
278
|
options.push(answer(id, { id: 'no', label: 'no', action: 'no', binds }));
|
|
272
279
|
options.push(answer(id, { id: 'later', label: 'not now', action: 'later', binds }));
|
|
273
280
|
}
|
|
274
|
-
|
|
275
|
-
options.push(nav({ id: 'back', label: 'the whole team', action: 'back' }));
|
|
276
|
-
if (total > 1) options.push(nav({ id: 'next', label: 'next', action: 'flip', binds: { step: 1 } }));
|
|
281
|
+
options.push(nav({ id: 'back', label: 'back to the team', action: 'back' }));
|
|
277
282
|
return {
|
|
278
283
|
id, kind: 'deck', asks: id,
|
|
279
284
|
title: card.yes ? card.yes.what : card.rows[0].work,
|
|
280
285
|
body: card.yes ? card.yes.why : deckLine(card),
|
|
281
286
|
line: card.said || null,
|
|
282
|
-
|
|
287
|
+
// a place with its unit, so it never reads as "1 of 4 needs you"
|
|
288
|
+
count: `job ${at + 1} of ${total}`,
|
|
283
289
|
label: card.label,
|
|
284
290
|
time: card.time || null,
|
|
285
291
|
job,
|
|
286
|
-
|
|
292
|
+
// each row's status too, so its cell reads in the same color as on the team page
|
|
293
|
+
table: { columns, rows: card.rows.map(r => columns.map(k => r[k] || '')), status: card.rows.map(r => r.status || '') },
|
|
287
294
|
options,
|
|
288
295
|
input: { ...SAY, label: `talk to ${card.member}`, placeholder: `talk to ${card.member}`, action: 'talk' },
|
|
289
296
|
member: card.member,
|
|
@@ -294,7 +301,7 @@ function deck({ id, at, total, card, columns, job = null }) {
|
|
|
294
301
|
|
|
295
302
|
// The swap card renders the swap list as it comes: Atris models, then the
|
|
296
303
|
// owner's picks, then "more models", which turns this card's options into
|
|
297
|
-
// the full list. A list with no picks opens short too: the model in use and
|
|
304
|
+
// the full list, with the models already shown first, where they were. A list with no picks opens short too: the model in use and
|
|
298
305
|
// the next two, then more models, when that hides at least two. Without the
|
|
299
306
|
// list, every model by tool.
|
|
300
307
|
const SHORT_LIST = 3;
|
|
@@ -308,7 +315,13 @@ function swapChoices(choices, all = false) {
|
|
|
308
315
|
more = { more: true, label: 'more models', options: list };
|
|
309
316
|
list = [...list.filter(o => o.current), ...list.filter(o => !o.current)].slice(0, SHORT_LIST);
|
|
310
317
|
}
|
|
311
|
-
if (all && more)
|
|
318
|
+
if (all && more) {
|
|
319
|
+
// the models already on the card keep their places, and the rest come in under them
|
|
320
|
+
const same = (a, b) => a.engine === b.engine && a.model === b.model;
|
|
321
|
+
const full = more.options || [];
|
|
322
|
+
const kept = list.map(o => full.find(f => same(f, o))).filter(Boolean);
|
|
323
|
+
return { list: [...kept, ...full.filter(f => !kept.includes(f))], more: null };
|
|
324
|
+
}
|
|
312
325
|
return { list, more };
|
|
313
326
|
}
|
|
314
327
|
|
|
@@ -317,7 +330,9 @@ function swapChoices(choices, all = false) {
|
|
|
317
330
|
function swap({ id, name, choices, scope = 'member', all = false, line = null }) {
|
|
318
331
|
const { list, more } = swapChoices(choices, all);
|
|
319
332
|
const options = list.map((m) => {
|
|
320
|
-
|
|
333
|
+
// the tool under the model, unless it is the same word (codex on codex)
|
|
334
|
+
const tool = m.tool && String(m.tool).trim().toLowerCase() !== String(m.label || '').trim().toLowerCase() ? m.tool : null;
|
|
335
|
+
const detail = m.current ? 'now' : [tool, m.record].filter(Boolean).join(', ') || null;
|
|
321
336
|
return answer(id, { id: `m-${hash8([m.engine, m.model, m.label])}`, label: m.label, detail, action: m.current ? 'none' : 'swap_to', style: 'choice', chosen: Boolean(m.current), binds: { engine: m.engine, model: m.model, scope } });
|
|
322
337
|
});
|
|
323
338
|
if (more) options.push(nav({ id: 'more', label: more.label || 'more models', action: 'more' }));
|
|
@@ -381,10 +396,22 @@ function sequenceProblems(cards) {
|
|
|
381
396
|
// Words the owner reads say what a thing is, not where a number comes from:
|
|
382
397
|
// "300 more npm installs" reads "300 more installs".
|
|
383
398
|
const ownerText = t => (typeof t === 'string' ? t.replace(/\bnpm (installs?|downloads?)\b/gi, '$1') : t);
|
|
399
|
+
// Every card speaks lowercase, so an idea or a job written "Write the page"
|
|
400
|
+
// reads "write the page" like the cards around it. Only a plain capitalized
|
|
401
|
+
// first word moves: a capital, then lowercase letters, and no other capital
|
|
402
|
+
// before the next space. OpenAI, GitHub, YouTube, McKinsey, API, and CRM stay
|
|
403
|
+
// as written, and iPhone starts lowercase already.
|
|
404
|
+
const lowerFirst = t => (typeof t === 'string' && /^[A-Z][a-z]+[^A-Z\s]*(?=\s|$)/.test(t) ? t[0].toLowerCase() + t.slice(1) : t);
|
|
405
|
+
// Only the words that are an idea or a job move. A talk card, a swap card,
|
|
406
|
+
// and the team page are titled with a name, and member and model names in
|
|
407
|
+
// cells and buttons are never changed: only the deck's work cell and the
|
|
408
|
+
// ideas on a choice card are sentences of that kind.
|
|
409
|
+
const NAMED = new Set(['member', 'swap', 'team']);
|
|
384
410
|
function ownerWords(card) {
|
|
385
|
-
const
|
|
386
|
-
out
|
|
387
|
-
|
|
411
|
+
const title = ownerText(card.title);
|
|
412
|
+
const out = { ...card, title: NAMED.has(card.kind) ? title : lowerFirst(title), body: ownerText(card.body), line: ownerText(card.line) };
|
|
413
|
+
out.options = (card.options || []).map(o => ({ ...o, label: card.kind === 'choice' && o.style === 'option' ? lowerFirst(ownerText(o.label)) : ownerText(o.label), ...(o.detail ? { detail: ownerText(o.detail) } : {}) }));
|
|
414
|
+
if (card.table) out.table = { ...card.table, rows: card.table.rows.map(row => row.map((cell, j) => (card.table.columns[j] === 'work' ? lowerFirst(ownerText(cell)) : ownerText(cell)))) };
|
|
388
415
|
return out;
|
|
389
416
|
}
|
|
390
417
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The phone page's icon: the lava orb's amber on the page's dark, small. An
|
|
4
|
+
// svg for the browser tab, and a 180px png for the home screen (iPhones take
|
|
5
|
+
// only a png there), drawn here with node's own zlib, no packages. Both go
|
|
6
|
+
// into the page inline as data, so no request for them ever needs the link's key.
|
|
7
|
+
|
|
8
|
+
const zlib = require('node:zlib');
|
|
9
|
+
|
|
10
|
+
const BG = [0x14, 0x11, 0x10];
|
|
11
|
+
// the lava's colors, lit from the upper left: cream where lit, amber, then deep
|
|
12
|
+
const LIT = [255, 232, 170];
|
|
13
|
+
const AMBER = [251, 150, 44];
|
|
14
|
+
const DEEP = [150, 52, 14];
|
|
15
|
+
|
|
16
|
+
const SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">'
|
|
17
|
+
+ '<defs><radialGradient id="o" cx="40%" cy="38%" r="66%">'
|
|
18
|
+
+ '<stop offset="0" stop-color="#ffe8aa"/><stop offset=".38" stop-color="#fb962c"/><stop offset="1" stop-color="#96340e"/>'
|
|
19
|
+
+ '</radialGradient></defs>'
|
|
20
|
+
+ '<rect width="64" height="64" rx="14" fill="#141110"/><circle cx="32" cy="32" r="20" fill="url(#o)"/></svg>';
|
|
21
|
+
|
|
22
|
+
const iconSvg = () => `data:image/svg+xml,${encodeURIComponent(SVG)}`;
|
|
23
|
+
|
|
24
|
+
const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t);
|
|
25
|
+
const clamp01 = v => Math.max(0, Math.min(1, v));
|
|
26
|
+
|
|
27
|
+
// one pixel of the orb at (x, y) on a size-wide square, as [r, g, b]
|
|
28
|
+
function orbPixel(x, y, size) {
|
|
29
|
+
const c = size / 2, r = size * 0.33;
|
|
30
|
+
const d = Math.hypot(x - c, y - c);
|
|
31
|
+
// the lit point sits up and to the left; the color runs from it outward
|
|
32
|
+
const lit = Math.hypot(x - size * 0.43, y - size * 0.41) / (r * 1.32);
|
|
33
|
+
const face = lit < 0.38 ? mix(LIT, AMBER, lit / 0.38) : mix(AMBER, DEEP, clamp01((lit - 0.38) / 0.62));
|
|
34
|
+
// a soft glow just past the edge, then the page
|
|
35
|
+
const glow = mix(BG, AMBER, 0.16 * Math.pow(clamp01(1 - (d - r) / (r * 0.5)), 2));
|
|
36
|
+
const edge = clamp01(r + 0.5 - d);
|
|
37
|
+
return mix(glow, face, edge).map(Math.round);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function chunk(type, data) {
|
|
41
|
+
const len = Buffer.alloc(4);
|
|
42
|
+
len.writeUInt32BE(data.length);
|
|
43
|
+
const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
|
44
|
+
const crc = Buffer.alloc(4);
|
|
45
|
+
crc.writeUInt32BE(zlib.crc32(body));
|
|
46
|
+
return Buffer.concat([len, body, crc]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// a size-wide square png of the orb, rgb with no alpha (the home screen fills
|
|
50
|
+
// see-through corners with black), each row filtered by its left neighbour so
|
|
51
|
+
// the smooth color compresses small
|
|
52
|
+
function orbPng(size = 180) {
|
|
53
|
+
const raw = Buffer.alloc(size * (1 + size * 3));
|
|
54
|
+
for (let y = 0; y < size; y++) {
|
|
55
|
+
const row = y * (1 + size * 3);
|
|
56
|
+
raw[row] = 1;
|
|
57
|
+
let prev = [0, 0, 0];
|
|
58
|
+
for (let x = 0; x < size; x++) {
|
|
59
|
+
const px = orbPixel(x + 0.5, y + 0.5, size);
|
|
60
|
+
for (let k = 0; k < 3; k++) raw[row + 1 + x * 3 + k] = (px[k] - prev[k]) & 0xff;
|
|
61
|
+
prev = px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const head = Buffer.alloc(13);
|
|
65
|
+
head.writeUInt32BE(size, 0);
|
|
66
|
+
head.writeUInt32BE(size, 4);
|
|
67
|
+
head[8] = 8;
|
|
68
|
+
head[9] = 2;
|
|
69
|
+
return Buffer.concat([
|
|
70
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
71
|
+
chunk('IHDR', head),
|
|
72
|
+
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
|
73
|
+
chunk('IEND', Buffer.alloc(0)),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let touch = null;
|
|
78
|
+
const touchIconPng = () => (touch || (touch = `data:image/png;base64,${orbPng(180).toString('base64')}`));
|
|
79
|
+
|
|
80
|
+
module.exports = { iconSvg, touchIconPng };
|