atris 3.71.3 → 3.72.1

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.
Files changed (46) hide show
  1. package/atris/skills/campaigns/SKILL.md +1 -1
  2. package/bin/atris.js +7 -1
  3. package/commands/auth.js +29 -2
  4. package/commands/autoland.js +102 -2
  5. package/commands/campaign.js +86 -13
  6. package/commands/clean.js +60 -4
  7. package/commands/gm.js +1273 -7
  8. package/commands/member.js +28 -3
  9. package/commands/mission.js +72 -23
  10. package/commands/sync.js +2 -2
  11. package/commands/task.js +49 -13
  12. package/commands/zen.js +183 -0
  13. package/lib/auto-accept-certified.js +9 -4
  14. package/lib/campaign-cadence.js +15 -5
  15. package/lib/campaign-core.js +44 -17
  16. package/lib/campaign-loop.js +45 -14
  17. package/lib/campaign-reading.js +14 -1
  18. package/lib/campaign-rsi.js +28 -11
  19. package/lib/campaign-schedule.js +124 -0
  20. package/lib/campaign-store.js +34 -2
  21. package/lib/campaign-trace.js +40 -14
  22. package/lib/engine-resource.js +5 -1
  23. package/lib/gm-bet.js +2219 -0
  24. package/lib/gm-phone-page.js +1102 -0
  25. package/lib/gm-phone.js +682 -0
  26. package/lib/gm-season.js +1162 -0
  27. package/lib/gm-target.js +292 -0
  28. package/lib/job-code-verify.js +169 -0
  29. package/lib/known-commands.js +1 -1
  30. package/lib/member-autoland.js +105 -14
  31. package/lib/member-progress.js +2 -2
  32. package/lib/member-report.js +11 -1
  33. package/lib/member-self-next-job.js +6 -1
  34. package/lib/member-up.js +17 -8
  35. package/lib/merged-review-close.js +206 -0
  36. package/lib/mission-protected-lane.js +52 -4
  37. package/lib/qr.js +338 -0
  38. package/lib/site-publish.js +16 -2
  39. package/lib/site.js +17 -7
  40. package/lib/yes-lock.js +530 -0
  41. package/lib/zen-apply.js +619 -0
  42. package/lib/zen-workspace.js +121 -0
  43. package/lib/zen.js +409 -0
  44. package/package.json +1 -1
  45. package/utils/api.js +15 -2
  46. package/utils/auth.js +8 -7
package/lib/qr.js ADDED
@@ -0,0 +1,338 @@
1
+ 'use strict';
2
+
3
+ // A small QR code encoder: byte mode, error correction level M, versions 1
4
+ // to 10 (up to 213 bytes, plenty for a link). Follows the QR spec (ISO 18004)
5
+ // the same way most small encoders do: pick the smallest version that fits,
6
+ // add Reed-Solomon error correction per block, interleave, place the bits,
7
+ // then try all eight masks and keep the one with the lowest penalty.
8
+ //
9
+ // encodeQr(text) -> { version, size, mask, modules } where modules[y][x] is
10
+ // true for a dark square. qrLines(text) renders it for a terminal with half
11
+ // block characters, two rows of squares per line, quiet zone included.
12
+
13
+ const MAX_VERSION = 10;
14
+ // Level M tables, index = version (index 0 unused).
15
+ const ECC_PER_BLOCK = [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26];
16
+ const ECC_BLOCKS = [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5];
17
+ const FORMAT_BITS_M = 0;
18
+
19
+ function rawDataModules(ver) {
20
+ let result = (16 * ver + 128) * ver + 64;
21
+ if (ver >= 2) {
22
+ const numAlign = Math.floor(ver / 7) + 2;
23
+ result -= (25 * numAlign - 10) * numAlign - 55;
24
+ if (ver >= 7) result -= 36;
25
+ }
26
+ return result;
27
+ }
28
+
29
+ function dataCodewords(ver) {
30
+ return Math.floor(rawDataModules(ver) / 8) - ECC_PER_BLOCK[ver] * ECC_BLOCKS[ver];
31
+ }
32
+
33
+ // Multiply in GF(2^8) with the QR polynomial x^8 + x^4 + x^3 + x^2 + 1.
34
+ function gfMul(x, y) {
35
+ let z = 0;
36
+ for (let i = 7; i >= 0; i -= 1) {
37
+ z = (z << 1) ^ ((z >>> 7) * 0x11d);
38
+ z ^= ((y >>> i) & 1) * x;
39
+ }
40
+ return z & 0xff;
41
+ }
42
+
43
+ function rsDivisor(degree) {
44
+ const result = new Array(degree).fill(0);
45
+ result[degree - 1] = 1;
46
+ let root = 1;
47
+ for (let i = 0; i < degree; i += 1) {
48
+ for (let j = 0; j < result.length; j += 1) {
49
+ result[j] = gfMul(result[j], root);
50
+ if (j + 1 < result.length) result[j] ^= result[j + 1];
51
+ }
52
+ root = gfMul(root, 0x02);
53
+ }
54
+ return result;
55
+ }
56
+
57
+ function rsRemainder(data, divisor) {
58
+ const result = new Array(divisor.length).fill(0);
59
+ for (const b of data) {
60
+ const factor = b ^ result.shift();
61
+ result.push(0);
62
+ divisor.forEach((coef, i) => { result[i] ^= gfMul(coef, factor); });
63
+ }
64
+ return result;
65
+ }
66
+
67
+ function getBit(x, i) {
68
+ return ((x >>> i) & 1) !== 0;
69
+ }
70
+
71
+ // Data bits for byte mode, padded to the version's data capacity.
72
+ function dataBytes(bytes, ver) {
73
+ const bits = [];
74
+ const push = (value, len) => {
75
+ for (let i = len - 1; i >= 0; i -= 1) bits.push((value >>> i) & 1);
76
+ };
77
+ push(0b0100, 4);
78
+ push(bytes.length, ver <= 9 ? 8 : 16);
79
+ for (const b of bytes) push(b, 8);
80
+ const capacity = dataCodewords(ver) * 8;
81
+ push(0, Math.min(4, capacity - bits.length));
82
+ push(0, (8 - (bits.length % 8)) % 8);
83
+ for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11) push(pad, 8);
84
+ const out = [];
85
+ for (let i = 0; i < bits.length; i += 8) {
86
+ let byte = 0;
87
+ for (let j = 0; j < 8; j += 1) byte = (byte << 1) | bits[i + j];
88
+ out.push(byte);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ // Split into blocks, add error correction to each, and interleave.
94
+ function withEcc(data, ver) {
95
+ const numBlocks = ECC_BLOCKS[ver];
96
+ const eccLen = ECC_PER_BLOCK[ver];
97
+ const raw = Math.floor(rawDataModules(ver) / 8);
98
+ const numShort = numBlocks - (raw % numBlocks);
99
+ const shortLen = Math.floor(raw / numBlocks);
100
+ const divisor = rsDivisor(eccLen);
101
+ const blocks = [];
102
+ for (let i = 0, k = 0; i < numBlocks; i += 1) {
103
+ const dat = data.slice(k, k + shortLen - eccLen + (i < numShort ? 0 : 1));
104
+ k += dat.length;
105
+ const ecc = rsRemainder(dat, divisor);
106
+ if (i < numShort) dat.push(0);
107
+ blocks.push(dat.concat(ecc));
108
+ }
109
+ const result = [];
110
+ for (let i = 0; i < blocks[0].length; i += 1) {
111
+ blocks.forEach((block, j) => {
112
+ if (i !== shortLen - eccLen || j >= numShort) result.push(block[i]);
113
+ });
114
+ }
115
+ return result;
116
+ }
117
+
118
+ function alignmentPositions(ver) {
119
+ if (ver === 1) return [];
120
+ const size = ver * 4 + 17;
121
+ const numAlign = Math.floor(ver / 7) + 2;
122
+ const step = Math.ceil((ver * 4 + 4) / (numAlign * 2 - 2)) * 2;
123
+ const result = [6];
124
+ for (let pos = size - 7; result.length < numAlign; pos -= step) result.splice(1, 0, pos);
125
+ return result;
126
+ }
127
+
128
+ function makeGrid(ver) {
129
+ const size = ver * 4 + 17;
130
+ const modules = Array.from({ length: size }, () => new Array(size).fill(false));
131
+ const isFunction = Array.from({ length: size }, () => new Array(size).fill(false));
132
+ const set = (x, y, dark) => {
133
+ modules[y][x] = dark;
134
+ isFunction[y][x] = true;
135
+ };
136
+ return { ver, size, modules, isFunction, set };
137
+ }
138
+
139
+ function drawFormatBits(grid, mask) {
140
+ const { size, set } = grid;
141
+ const data = (FORMAT_BITS_M << 3) | mask;
142
+ let rem = data;
143
+ for (let i = 0; i < 10; i += 1) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
144
+ const bits = ((data << 10) | rem) ^ 0x5412;
145
+ for (let i = 0; i <= 5; i += 1) set(8, i, getBit(bits, i));
146
+ set(8, 7, getBit(bits, 6));
147
+ set(8, 8, getBit(bits, 7));
148
+ set(7, 8, getBit(bits, 8));
149
+ for (let i = 9; i < 15; i += 1) set(14 - i, 8, getBit(bits, i));
150
+ for (let i = 0; i < 8; i += 1) set(size - 1 - i, 8, getBit(bits, i));
151
+ for (let i = 8; i < 15; i += 1) set(8, size - 15 + i, getBit(bits, i));
152
+ set(8, size - 8, true);
153
+ }
154
+
155
+ function drawVersion(grid) {
156
+ const { ver, size, set } = grid;
157
+ if (ver < 7) return;
158
+ let rem = ver;
159
+ for (let i = 0; i < 12; i += 1) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
160
+ const bits = (ver << 12) | rem;
161
+ for (let i = 0; i < 18; i += 1) {
162
+ const bit = getBit(bits, i);
163
+ const a = size - 11 + (i % 3);
164
+ const b = Math.floor(i / 3);
165
+ set(a, b, bit);
166
+ set(b, a, bit);
167
+ }
168
+ }
169
+
170
+ function drawFunctionPatterns(grid) {
171
+ const { size, set } = grid;
172
+ for (let i = 0; i < size; i += 1) {
173
+ set(6, i, i % 2 === 0);
174
+ set(i, 6, i % 2 === 0);
175
+ }
176
+ const finder = (x, y) => {
177
+ for (let dy = -4; dy <= 4; dy += 1) {
178
+ for (let dx = -4; dx <= 4; dx += 1) {
179
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
180
+ const xx = x + dx;
181
+ const yy = y + dy;
182
+ if (xx >= 0 && xx < size && yy >= 0 && yy < size) set(xx, yy, dist !== 2 && dist !== 4);
183
+ }
184
+ }
185
+ };
186
+ finder(3, 3);
187
+ finder(size - 4, 3);
188
+ finder(3, size - 4);
189
+ const pos = alignmentPositions(grid.ver);
190
+ const n = pos.length;
191
+ for (let i = 0; i < n; i += 1) {
192
+ for (let j = 0; j < n; j += 1) {
193
+ if ((i === 0 && j === 0) || (i === 0 && j === n - 1) || (i === n - 1 && j === 0)) continue;
194
+ for (let dy = -2; dy <= 2; dy += 1) {
195
+ for (let dx = -2; dx <= 2; dx += 1) set(pos[i] + dx, pos[j] + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
196
+ }
197
+ }
198
+ }
199
+ drawFormatBits(grid, 0);
200
+ drawVersion(grid);
201
+ }
202
+
203
+ function drawCodewords(grid, data) {
204
+ const { size, modules, isFunction } = grid;
205
+ let i = 0;
206
+ for (let right = size - 1; right >= 1; right -= 2) {
207
+ if (right === 6) right = 5;
208
+ for (let vert = 0; vert < size; vert += 1) {
209
+ for (let j = 0; j < 2; j += 1) {
210
+ const x = right - j;
211
+ const upward = ((right + 1) & 2) === 0;
212
+ const y = upward ? size - 1 - vert : vert;
213
+ if (!isFunction[y][x] && i < data.length * 8) {
214
+ modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
215
+ i += 1;
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ const MASKS = [
223
+ (x, y) => (x + y) % 2 === 0,
224
+ (x, y) => y % 2 === 0,
225
+ x => x % 3 === 0,
226
+ (x, y) => (x + y) % 3 === 0,
227
+ (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
228
+ (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
229
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
230
+ (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
231
+ ];
232
+
233
+ function applyMask(grid, mask) {
234
+ const { size, modules, isFunction } = grid;
235
+ const test = MASKS[mask];
236
+ for (let y = 0; y < size; y += 1) {
237
+ for (let x = 0; x < size; x += 1) {
238
+ if (!isFunction[y][x] && test(x, y)) modules[y][x] = !modules[y][x];
239
+ }
240
+ }
241
+ }
242
+
243
+ // The spec's four penalty rules: runs of 5+ in a line, 2x2 blocks, finder
244
+ // look-alikes (1:1:3:1:1 with 4 light on a side), and dark/light balance.
245
+ function penalty(modules) {
246
+ const size = modules.length;
247
+ let score = 0;
248
+ const line = get => {
249
+ let run = 1;
250
+ for (let i = 1; i < size; i += 1) {
251
+ if (get(i) === get(i - 1)) {
252
+ run += 1;
253
+ if (run === 5) score += 3;
254
+ else if (run > 5) score += 1;
255
+ } else {
256
+ run = 1;
257
+ }
258
+ }
259
+ for (let i = 0; i + 10 < size + 4; i += 1) {
260
+ const at = k => (i + k >= 0 && i + k < size ? get(i + k) : false);
261
+ const core = at(0) && !at(1) && at(2) && at(3) && at(4) && !at(5) && at(6);
262
+ if (!core) continue;
263
+ const lightBefore = [1, 2, 3, 4].every(k => !at(-k));
264
+ const lightAfter = [7, 8, 9, 10].every(k => !at(k));
265
+ if (lightBefore || lightAfter) score += 40;
266
+ }
267
+ };
268
+ for (let y = 0; y < size; y += 1) line(x => modules[y][x]);
269
+ for (let x = 0; x < size; x += 1) line(y => modules[y][x]);
270
+ for (let y = 0; y < size - 1; y += 1) {
271
+ for (let x = 0; x < size - 1; x += 1) {
272
+ const c = modules[y][x];
273
+ if (c === modules[y][x + 1] && c === modules[y + 1][x] && c === modules[y + 1][x + 1]) score += 3;
274
+ }
275
+ }
276
+ let dark = 0;
277
+ for (const row of modules) for (const m of row) if (m) dark += 1;
278
+ const total = size * size;
279
+ const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
280
+ score += Math.max(0, k) * 10;
281
+ return score;
282
+ }
283
+
284
+ function encodeQr(text, { mask: forcedMask = null } = {}) {
285
+ const bytes = [...Buffer.from(String(text), 'utf8')];
286
+ let ver = 1;
287
+ for (; ver <= MAX_VERSION; ver += 1) {
288
+ const needed = 4 + (ver <= 9 ? 8 : 16) + bytes.length * 8;
289
+ if (needed <= dataCodewords(ver) * 8) break;
290
+ }
291
+ if (ver > MAX_VERSION) throw new Error(`text too long for a QR code here (${bytes.length} bytes, max 213)`);
292
+ const grid = makeGrid(ver);
293
+ drawFunctionPatterns(grid);
294
+ drawCodewords(grid, withEcc(dataBytes(bytes, ver), ver));
295
+ let mask = forcedMask;
296
+ if (mask == null) {
297
+ let best = Infinity;
298
+ for (let m = 0; m < 8; m += 1) {
299
+ applyMask(grid, m);
300
+ drawFormatBits(grid, m);
301
+ const p = penalty(grid.modules);
302
+ if (p < best) {
303
+ best = p;
304
+ mask = m;
305
+ }
306
+ applyMask(grid, m);
307
+ }
308
+ }
309
+ applyMask(grid, mask);
310
+ drawFormatBits(grid, mask);
311
+ return { version: ver, size: grid.size, mask, modules: grid.modules };
312
+ }
313
+
314
+ // Terminal art: two rows of squares per line with half block characters.
315
+ // With color on (the default) each line forces black on bright white, so the
316
+ // code scans the same on a dark or a light terminal. With color off, light
317
+ // squares print as blocks, which reads right on a dark terminal.
318
+ const FULL = '█';
319
+ const UPPER = '▀';
320
+ const LOWER = '▄';
321
+
322
+ function qrLines(text, { quiet = 2, color = true } = {}) {
323
+ const { modules, size } = encodeQr(text);
324
+ const at = (x, y) => (x >= 0 && y >= 0 && x < size && y < size ? modules[y][x] : false);
325
+ const lines = [];
326
+ for (let y = -quiet; y < size + quiet; y += 2) {
327
+ let line = '';
328
+ for (let x = -quiet; x < size + quiet; x += 1) {
329
+ const top = color ? at(x, y) : !at(x, y);
330
+ const bottom = color ? at(x, y + 1) : !at(x, y + 1);
331
+ line += top && bottom ? FULL : top ? UPPER : bottom ? LOWER : ' ';
332
+ }
333
+ lines.push(color ? `\x1b[30;107m${line}\x1b[0m` : line);
334
+ }
335
+ return lines;
336
+ }
337
+
338
+ module.exports = { encodeQr, qrLines, rsDivisor, rsRemainder, gfMul, dataCodewords, alignmentPositions };
@@ -322,15 +322,29 @@ async function run(argv, deps = {}) {
322
322
  let response;
323
323
  let parsed;
324
324
  try {
325
- response = await fetchFn(url, {
325
+ const requestOptions = {
326
326
  method: 'POST',
327
+ redirect: 'manual',
327
328
  headers: {
328
329
  Authorization: `Bearer ${token}`,
329
330
  Accept: 'application/json',
330
331
  'Content-Type': 'application/json',
331
332
  },
332
333
  body: JSON.stringify(body),
333
- });
334
+ };
335
+ let requestUrl = url;
336
+ for (let redirects = 0; ; redirects++) {
337
+ response = await fetchFn(requestUrl, requestOptions);
338
+ const location = response.headers && response.headers.get('location');
339
+ if (![301, 302, 303, 307, 308].includes(response.status) || !location) break;
340
+ if (response.body) await response.body.cancel();
341
+ const target = new URL(location, requestUrl);
342
+ if (target.origin !== new URL(url).origin) {
343
+ throw new Error(`refusing cross-origin redirect to ${target.host || target.protocol}`);
344
+ }
345
+ if (redirects >= 20) throw new Error('too many redirects');
346
+ requestUrl = target.toString();
347
+ }
334
348
  parsed = await readResponse(response);
335
349
  } catch (error) {
336
350
  errorLog(`publish failed: ${error.message}`);
package/lib/site.js CHANGED
@@ -97,18 +97,28 @@ const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': '
97
97
  // minimal static preview server for a built site dir (no deps)
98
98
  function serveSite(dir, port = 4321) {
99
99
  const root = path.resolve(dir);
100
+ let realRoot = root;
101
+ try { realRoot = fs.realpathSync(root); } catch { /* missing folder: every request 404s */ }
100
102
  const server = http.createServer((req, res) => {
101
- let rel = decodeURIComponent((req.url || '/').split('?')[0]);
103
+ let rel;
104
+ try { rel = decodeURIComponent((req.url || '/').split('?')[0]); }
105
+ catch { res.writeHead(400); res.end('bad request'); return; }
102
106
  if (rel.endsWith('/')) rel += 'index.html';
103
107
  const file = path.normalize(path.join(root, rel));
104
- if (!file.startsWith(root)) { res.writeHead(403); res.end('forbidden'); return; }
105
- fs.readFile(file, (err, data) => {
106
- if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('not found'); return; }
107
- res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' });
108
- res.end(data);
108
+ // compare against root + separator so a sibling like "site-evil" can't pass for "site"
109
+ if (file !== root && !file.startsWith(root + path.sep)) { res.writeHead(403); res.end('forbidden'); return; }
110
+ // Resolve symlinks too: a link inside the site folder must not reach files outside it.
111
+ fs.realpath(file, (linkErr, real) => {
112
+ if (linkErr) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('not found'); return; }
113
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep)) { res.writeHead(403); res.end('forbidden'); return; }
114
+ fs.readFile(real, (err, data) => {
115
+ if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('not found'); return; }
116
+ res.writeHead(200, { 'Content-Type': MIME[path.extname(real)] || 'application/octet-stream' });
117
+ res.end(data);
118
+ });
109
119
  });
110
120
  });
111
- return new Promise((resolve) => server.listen(port, () => resolve({ server, url: `http://localhost:${port}` })));
121
+ return new Promise((resolve) => server.listen(port, '127.0.0.1', () => resolve({ server, url: `http://localhost:${port}` })));
112
122
  }
113
123
 
114
124
  module.exports = { buildSite, serveSite, collectMd, slugFor, firstHeading, firstParagraph };