great-cto 2.77.1 → 2.78.0

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.
@@ -2,7 +2,7 @@
2
2
  "name": "great_cto",
3
3
  "id": "great_cto",
4
4
  "description": "Engineering process for solo founders and teams up to 50 engineers. Agents do architecture, code review, QA, and security. You make two decisions per feature.",
5
- "version": "2.77.1",
5
+ "version": "2.78.0",
6
6
  "author": {
7
7
  "name": "Great CTO",
8
8
  "url": "https://github.com/avelikiy/great_cto"
@@ -16,6 +16,7 @@ import { getTasks } from './beads.mjs';
16
16
  import { readVerdicts } from './verdicts.mjs';
17
17
  import { isFailure } from './fleet.mjs';
18
18
  import { getShareState, toggleShare } from './share.mjs';
19
+ import { log } from './log.mjs';
19
20
 
20
21
  // ── Email alerts (Resend) ─────────────────────────────────────────────────
21
22
  // Dispatch model: read webhooks.json on every fire (idempotent if disabled
@@ -91,12 +92,12 @@ async function fireEmailAlert(eventName, dedupeKey, payload) {
91
92
  });
92
93
  if (!res.ok) {
93
94
  const txt = await res.text().catch(() => '');
94
- console.warn(`fireEmailAlert ${eventName}: relay HTTP ${res.status} ${txt.slice(0, 200)}`);
95
+ log.warn(`fireEmailAlert ${eventName}: relay HTTP ${res.status} ${txt.slice(0, 200)}`);
95
96
  return;
96
97
  }
97
- console.log(`fireEmailAlert: sent ${eventName} (${dedupeKey})`);
98
+ log.info(`fireEmailAlert: sent ${eventName} (${dedupeKey})`);
98
99
  } catch (e) {
99
- console.warn(`fireEmailAlert ${eventName} failed:`, e.message);
100
+ log.warn(`fireEmailAlert ${eventName} failed:`, e.message);
100
101
  }
101
102
  }
102
103
 
@@ -119,12 +120,12 @@ async function firePushAlert(eventName, dedupeKey, payload) {
119
120
  catch (e) {
120
121
  // 410 = subscription expired — browser unsubscribed, clean up
121
122
  if (e.statusCode === 410) removeSubscription(PUSH_SUBS_FILE, sub.endpoint);
122
- else console.warn(`firePushAlert send failed for ${sub.endpoint}:`, e.message);
123
+ else log.warn(`firePushAlert send failed for ${sub.endpoint}:`, e.message);
123
124
  }
124
125
  }
125
126
  fired[pushKey] = new Date().toISOString();
126
127
  writeAlertsFired(fired);
127
- } catch (e) { console.warn('firePushAlert failed:', e.message); }
128
+ } catch (e) { log.warn('firePushAlert failed:', e.message); }
128
129
  }
129
130
 
130
131
  // ── Cron: scan gates / cost / weekly digest ───────────────────────────────
@@ -172,7 +173,7 @@ function startAlertCron() {
172
173
  firePushAlert('incident.p0', dedupeKey, p0Payload);
173
174
  }
174
175
  }
175
- } catch (e) { console.warn('cron incident.p0 failed:', e.message); }
176
+ } catch (e) { log.warn('cron incident.p0 failed:', e.message); }
176
177
  }, FIVE_MIN);
177
178
 
178
179
  // gate.blocked: new BLOCKED verdict from security-officer
@@ -207,7 +208,7 @@ function startAlertCron() {
207
208
  addNotification('gate.blocked', blockedPayload);
208
209
  firePushAlert('gate.blocked', dedupeKey, blockedPayload);
209
210
  }
210
- } catch (e) { console.warn('cron gate.blocked failed:', e.message); }
211
+ } catch (e) { log.warn('cron gate.blocked failed:', e.message); }
211
212
  }, FIVE_MIN);
212
213
 
213
214
  // gate.stale: gate task open between 2h and 7 days.
@@ -238,7 +239,7 @@ function startAlertCron() {
238
239
  firePushAlert('gate.stale', dedupeKey, stalePayload);
239
240
  }
240
241
  }
241
- } catch (e) { console.warn('cron gate.stale failed:', e.message); }
242
+ } catch (e) { log.warn('cron gate.stale failed:', e.message); }
242
243
  }, FIVE_MIN);
243
244
 
244
245
  // cost.threshold: monthly LLM spend at 80% / 100% of budget
@@ -277,7 +278,7 @@ function startAlertCron() {
277
278
  firePushAlert('cost.threshold', dedupeKey, costPayload);
278
279
  }
279
280
  }
280
- } catch (e) { console.warn('cron cost.threshold failed:', e.message); }
281
+ } catch (e) { log.warn('cron cost.threshold failed:', e.message); }
281
282
  }, ONE_HOUR);
282
283
 
283
284
  // digest.weekly: Friday 09:00 UTC ± server tick
@@ -317,7 +318,7 @@ function startAlertCron() {
317
318
  addNotification('digest.weekly', weeklyPayload);
318
319
  await firePushAlert('digest.weekly', dedupeKey, weeklyPayload);
319
320
  }
320
- } catch (e) { console.warn('cron digest.weekly failed:', e.message); }
321
+ } catch (e) { log.warn('cron digest.weekly failed:', e.message); }
321
322
  }, FIVE_MIN);
322
323
 
323
324
  // digest.daily: morning summary (Mon–Fri, 08:00 UTC).
@@ -384,7 +385,7 @@ function startAlertCron() {
384
385
  addNotification('digest.daily', dailyPayload);
385
386
  await firePushAlert('digest.daily', dedupeKey, dailyPayload);
386
387
  }
387
- } catch (e) { console.warn('cron digest.daily failed:', e.message); }
388
+ } catch (e) { log.warn('cron digest.daily failed:', e.message); }
388
389
  }, FIVE_MIN);
389
390
 
390
391
  // report.daily: republish share reports every day at 09:00 UTC
@@ -401,13 +402,13 @@ function startAlertCron() {
401
402
  if (_reportRepublishDedupeSet.has(dedupeKey)) continue;
402
403
  _reportRepublishDedupeSet.add(dedupeKey);
403
404
  toggleShare(true, proj.path, true)
404
- .then(() => console.log(`report.daily: republished ${proj.slug}`))
405
- .catch(e => console.warn(`report.daily: ${proj.slug} failed: ${e.message}`));
405
+ .then(() => log.info(`report.daily: republished ${proj.slug}`))
406
+ .catch(e => log.warn(`report.daily: ${proj.slug} failed: ${e.message}`));
406
407
  }
407
- } catch (e) { console.warn('cron report.daily failed:', e.message); }
408
+ } catch (e) { log.warn('cron report.daily failed:', e.message); }
408
409
  }, FIVE_MIN);
409
410
 
410
- console.log('Alert cron started: gate.stale (5min), sla.escalate (5min), connector.health (5min), cost.threshold (1h), digest.daily (Mon–Fri 08:00), digest.weekly (Fri 09:00), report.daily (09:00)');
411
+ log.info('Alert cron started: gate.stale (5min), sla.escalate (5min), connector.health (5min), cost.threshold (1h), digest.daily (Mon–Fri 08:00), digest.weekly (Fri 09:00), report.daily (09:00)');
411
412
  }
412
413
 
413
414
  export { readAlertsFired, writeAlertsFired, fireEmailAlert, firePushAlert, startAlertCron };
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import { spawnSync } from 'child_process';
5
5
  import { bdCache } from './state.mjs';
6
+ import { log } from './log.mjs';
6
7
 
7
8
  // ── Beads data ─────────────────────────────────────────────────────────────────
8
9
  // Cache bdList output per cwd for BD_CACHE_TTL_MS. Invalidated when the project's
@@ -85,19 +86,26 @@ function checkBeadsAvailable(cwd) {
85
86
  let _bdWriteChain = Promise.resolve();
86
87
  function bdWriteSerialised(fn) {
87
88
  const next = _bdWriteChain.then(() => fn()).catch((e) => {
88
- console.error('[bd-write-serialised] error:', e?.message || e);
89
+ log.error('[bd-write-serialised]', e?.message || e);
89
90
  return null;
90
91
  });
91
92
  _bdWriteChain = next.then(() => undefined).catch(() => undefined);
92
93
  return next;
93
94
  }
94
95
 
95
- function bdList(cwd = process.cwd()) {
96
+ // On a transient bd failure (nonzero exit, dolt lock, timeout, throw), keep
97
+ // serving the last-good cached data instead of overwriting it with [] — an
98
+ // empty result is indistinguishable from "no tasks" and would wipe a
99
+ // populated board on every SSE push (great_cto-e2ew). We deliberately do NOT
100
+ // refresh cached.ts on failure, so the next call retries bd immediately
101
+ // rather than being TTL-gated on a failed read.
102
+ function bdList(cwd = process.cwd(), runner = bd) {
96
103
  const cached = bdCache.get(cwd);
97
104
  if (cached && Date.now() - cached.ts < BD_CACHE_TTL_MS) return cached.data;
98
105
  try {
99
- const result = bd(['list', '--json', '--all', '--include-gates'], { cwd });
106
+ const result = runner(['list', '--json', '--all', '--include-gates'], { cwd });
100
107
  if (result.status !== 0) {
108
+ if (cached) return cached.data; // last-good data, cache untouched
101
109
  bdCache.set(cwd, { ts: Date.now(), data: [] });
102
110
  return [];
103
111
  }
@@ -105,6 +113,7 @@ function bdList(cwd = process.cwd()) {
105
113
  bdCache.set(cwd, { ts: Date.now(), data });
106
114
  return data;
107
115
  } catch {
116
+ if (cached) return cached.data; // last-good data, cache untouched
108
117
  bdCache.set(cwd, { ts: Date.now(), data: [] });
109
118
  return [];
110
119
  }
@@ -21,7 +21,11 @@ const HOST = (() => {
21
21
  })();
22
22
  const GREAT_CTO_DIR = path.join(os.homedir(), '.great_cto');
23
23
  const SHARE_STATE_FILE = path.join(GREAT_CTO_DIR, 'board-share.json');
24
- const PROJECTS_FILE = path.join(GREAT_CTO_DIR, 'projects.json');
24
+ // Test seam: honor an explicit override so tests can point the registry at a
25
+ // tmp fixture without touching the real ~/.great_cto/projects.json (same
26
+ // convention as GREAT_CTO_BD_BIN in lib/beads.mjs). Unset in production —
27
+ // zero behavior change at runtime.
28
+ const PROJECTS_FILE = process.env.GREAT_CTO_PROJECTS_FILE || path.join(GREAT_CTO_DIR, 'projects.json');
25
29
  const SHARE_ENDPOINT = 'https://greatcto.systems/r/';
26
30
  const VAPID_KEYS_FILE = path.join(GREAT_CTO_DIR, 'vapid-keys.json');
27
31
  const PUSH_SUBS_FILE = path.join(GREAT_CTO_DIR, 'push-subscriptions.json');
@@ -0,0 +1,44 @@
1
+ // Zero-dep structured logger for the great_cto board server.
2
+ //
3
+ // Design:
4
+ // - Levels: debug < info < warn < error. Threshold read once from
5
+ // GREAT_CTO_LOG_LEVEL (default 'info'); unrecognized values fall back to 'info'.
6
+ // - Output: single-line `<ISO timestamp> <LEVEL> <message>` per call.
7
+ // - Routing: error → stderr; debug/info/warn → stdout. This matches Unix
8
+ // convention (stdout = normal operation, stderr = attention-worthy) and
9
+ // keeps the board's stdout free of interleaved error noise.
10
+ // - No external deps, ESM only, node: prefix for built-ins.
11
+
12
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
13
+
14
+ function currentThreshold() {
15
+ const raw = (process.env.GREAT_CTO_LOG_LEVEL || 'info').toLowerCase();
16
+ return LEVELS[raw] ?? LEVELS.info;
17
+ }
18
+
19
+ function format(level, args) {
20
+ const ts = new Date().toISOString();
21
+ const msg = args
22
+ .map(a => (typeof a === 'string' ? a : a instanceof Error ? (a.stack || a.message) : JSON.stringify(a)))
23
+ .join(' ');
24
+ return `${ts} ${level.toUpperCase()} ${msg}`;
25
+ }
26
+
27
+ function emit(level, args) {
28
+ if (LEVELS[level] < currentThreshold()) return;
29
+ const line = format(level, args);
30
+ if (level === 'error') {
31
+ process.stderr.write(line + '\n');
32
+ } else {
33
+ process.stdout.write(line + '\n');
34
+ }
35
+ }
36
+
37
+ export const log = {
38
+ debug: (...args) => emit('debug', args),
39
+ info: (...args) => emit('info', args),
40
+ warn: (...args) => emit('warn', args),
41
+ error: (...args) => emit('error', args),
42
+ };
43
+
44
+ export default log;
@@ -4,6 +4,20 @@ import os from 'os';
4
4
  import { spawnSync } from 'child_process';
5
5
  import { planGates } from '../../../scripts/lib/gate-plan.mjs';
6
6
  import { GREAT_CTO_DIR, PROJECTS_FILE } from './config.mjs';
7
+ import { isInsideDir } from './util.mjs';
8
+
9
+ // Same HOME-boundary policy /api/projects/register enforces (lib/routes.mjs):
10
+ // a raw absolute/tilde path must resolve inside the operator's home directory,
11
+ // no exceptions. Without this, a request like ?project=/etc or ?project=/tmp
12
+ // makes the board read/write project data (.beads, verdicts/, PROJECT.md)
13
+ // at an arbitrary filesystem location the operator never registered. Returns
14
+ // null (not process.cwd()) so callers can distinguish "not a raw path" from
15
+ // "raw path rejected" and fall back explicitly.
16
+ function resolveRawPathWithinHome(slugOrPath) {
17
+ const raw = slugOrPath.startsWith('~') ? slugOrPath.replace(/^~/, os.homedir()) : slugOrPath;
18
+ const resolved = path.resolve(raw);
19
+ return isInsideDir(os.homedir(), resolved) ? resolved : null;
20
+ }
7
21
 
8
22
  // ── Project registry ───────────────────────────────────────────────────────────
9
23
  function readProjectsRegistry() {
@@ -11,9 +25,44 @@ function readProjectsRegistry() {
11
25
  catch {}
12
26
  return { projects: [] };
13
27
  }
28
+ // Pick the best entry among several sharing a slug: prefer one whose path
29
+ // still exists on disk; among several existing (or several missing), prefer
30
+ // the most recently added_at. Never returns a non-existent path when an
31
+ // existing match is available (great_cto-7xfc).
32
+ function pickBestBySlug(candidates) {
33
+ let best = null;
34
+ for (const p of candidates) {
35
+ if (!best) { best = p; continue; }
36
+ const pExists = existsSafe(p.path);
37
+ const bestExists = existsSafe(best.path);
38
+ if (pExists && !bestExists) { best = p; continue; }
39
+ if (!pExists && bestExists) continue;
40
+ // Both exist or both missing — prefer most recent added_at.
41
+ if (String(p.added_at || '') > String(best.added_at || '')) best = p;
42
+ }
43
+ return best;
44
+ }
45
+ function existsSafe(p) {
46
+ try { return !!p && fs.existsSync(p); } catch { return false; }
47
+ }
48
+ // Dedupe registry entries by slug before writing, so the registry self-heals
49
+ // on every write regardless of how the duplicate got in (great_cto-7xfc).
50
+ function dedupeProjects(projects) {
51
+ const bySlug = new Map();
52
+ for (const p of projects) {
53
+ if (!p || !p.slug) continue;
54
+ const group = bySlug.get(p.slug) || [];
55
+ group.push(p);
56
+ bySlug.set(p.slug, group);
57
+ }
58
+ const out = [];
59
+ for (const group of bySlug.values()) out.push(pickBestBySlug(group));
60
+ return out;
61
+ }
14
62
  function writeProjectsRegistry(reg) {
15
63
  fs.mkdirSync(GREAT_CTO_DIR, { recursive: true });
16
- fs.writeFileSync(PROJECTS_FILE, JSON.stringify(reg, null, 2));
64
+ const deduped = { ...reg, projects: dedupeProjects(reg.projects || []) };
65
+ fs.writeFileSync(PROJECTS_FILE, JSON.stringify(deduped, null, 2));
17
66
  }
18
67
  // Map legacy / informal archetype names to canonical archetype slugs.
19
68
  // This lets the board correctly badge old PROJECT.md files (mobile, saas-platform,
@@ -99,10 +148,20 @@ function autoRegisterProject(dir) {
99
148
  const meta = readProjectMd(dir);
100
149
  if (!meta) return null;
101
150
  const reg = readProjectsRegistry();
102
- if (!reg.projects.find(p => p.path === meta.path)) {
151
+ const existingByPath = reg.projects.find(p => p.path === meta.path);
152
+ if (existingByPath) return meta; // already registered at this path, nothing to do
153
+ const existingBySlug = reg.projects.find(p => p.slug === meta.slug);
154
+ if (existingBySlug) {
155
+ // Repo moved: update the existing entry's path in place instead of
156
+ // inserting a second entry for the same slug (great_cto-7xfc).
157
+ existingBySlug.path = meta.path;
158
+ existingBySlug.archetype = meta.archetype;
159
+ existingBySlug.description = meta.description;
160
+ existingBySlug.added_at = meta.added_at;
161
+ } else {
103
162
  reg.projects.push(meta);
104
- writeProjectsRegistry(reg);
105
163
  }
164
+ writeProjectsRegistry(reg);
106
165
  return meta;
107
166
  }
108
167
 
@@ -206,12 +265,22 @@ function listProjects() {
206
265
  }
207
266
  function resolveProjectCwd(slugOrPath) {
208
267
  if (!slugOrPath) return process.cwd();
209
- // If absolute path or starts with ~, use directly
210
- if (slugOrPath.startsWith('/')) return slugOrPath;
211
- if (slugOrPath.startsWith('~')) return slugOrPath.replace(/^~/, os.homedir());
212
- // Else look up in registry by slug
268
+ // If absolute path or starts with ~, it's raw user input — enforce the same
269
+ // HOME-boundary policy /api/projects/register uses. A path outside HOME
270
+ // falls back to process.cwd() (same fallback already used below for an
271
+ // unknown slug) rather than being honored verbatim.
272
+ if (slugOrPath.startsWith('/') || slugOrPath.startsWith('~')) {
273
+ const withinHome = resolveRawPathWithinHome(slugOrPath);
274
+ return withinHome || process.cwd();
275
+ }
276
+ // Else look up in registry by slug — among duplicate slugs (great_cto-7xfc),
277
+ // prefer the entry whose path exists on disk; among several existing (or
278
+ // several missing), prefer the most recently added_at. Registry entries were
279
+ // already validated at registration time (register enforces the HOME
280
+ // boundary before writing), so slug resolution is untouched.
213
281
  const reg = readProjectsRegistry();
214
- const found = reg.projects.find(p => p.slug === slugOrPath);
282
+ const matches = reg.projects.filter(p => p.slug === slugOrPath);
283
+ const found = pickBestBySlug(matches);
215
284
  return found ? found.path : process.cwd();
216
285
  }
217
286
 
@@ -223,19 +292,22 @@ function resolveProjectCwd(slugOrPath) {
223
292
  *
224
293
  * resolved values:
225
294
  * 'cwd' — no project param passed; using server cwd as documented
226
- * 'path' — absolute / tilde path passed and used directly
295
+ * 'path' — absolute / tilde path passed, resolved inside HOME, used directly
227
296
  * 'slug' — slug found in registry
228
- * 'fallback' — slug requested but NOT in registry; using cwd as fallback.
229
- * Caller should warn the user (header + log).
297
+ * 'fallback' — slug requested but NOT in registry, OR a raw path was requested
298
+ * that resolves outside HOME (great_cto-qvg9); using cwd as
299
+ * fallback. Caller should warn the user (header + log).
230
300
  */
231
301
  function resolveProjectInfo(slugOrPath) {
232
302
  if (!slugOrPath) return { cwd: process.cwd(), resolved: 'cwd' };
233
- if (slugOrPath.startsWith('/')) return { cwd: slugOrPath, resolved: 'path' };
234
- if (slugOrPath.startsWith('~')) {
235
- return { cwd: slugOrPath.replace(/^~/, os.homedir()), resolved: 'path' };
303
+ if (slugOrPath.startsWith('/') || slugOrPath.startsWith('~')) {
304
+ const withinHome = resolveRawPathWithinHome(slugOrPath);
305
+ if (withinHome) return { cwd: withinHome, resolved: 'path' };
306
+ return { cwd: process.cwd(), resolved: 'fallback', requested: slugOrPath };
236
307
  }
237
308
  const reg = readProjectsRegistry();
238
- const found = reg.projects.find(p => p.slug === slugOrPath);
309
+ const matches = reg.projects.filter(p => p.slug === slugOrPath);
310
+ const found = pickBestBySlug(matches);
239
311
  if (found) return { cwd: found.path, resolved: 'slug' };
240
312
  return { cwd: process.cwd(), resolved: 'fallback', requested: slugOrPath };
241
313
  }
@@ -243,6 +315,8 @@ function resolveProjectInfo(slugOrPath) {
243
315
  export {
244
316
  readProjectsRegistry,
245
317
  writeProjectsRegistry,
318
+ pickBestBySlug,
319
+ dedupeProjects,
246
320
  ARCHETYPE_ALIASES,
247
321
  normalizeArchetype,
248
322
  extractArchetype,
@@ -6,13 +6,14 @@ import {
6
6
  addSubscription,
7
7
  removeSubscription,
8
8
  } from '../push-adapter.mjs';
9
- import { PORT, GREAT_CTO_DIR, VAPID_KEYS_FILE, PUSH_SUBS_FILE, BUILD_VERSION } from './config.mjs';
10
- import { eventSurface, readFileSafe } from './util.mjs';
9
+ import { GREAT_CTO_DIR, VAPID_KEYS_FILE, PUSH_SUBS_FILE, BUILD_VERSION } from './config.mjs';
10
+ import { eventSurface, readFileSafe, originAllowed } from './util.mjs';
11
11
  import { sseClients, notifHistory } from './state.mjs';
12
12
  import { autoRegisterProject, listProjects, resolveProjectCwd, getChangeTier } from './projects.mjs';
13
13
  import { broadcastTasks } from './sse.mjs';
14
14
  import { saveNotifHistory } from './notifications.mjs';
15
15
  import { getMemory, getPipeline, getCostHistory, getInbox } from './data-readers.mjs';
16
+ import { log } from './log.mjs';
16
17
  import { bdCacheInvalidate, checkBeadsAvailable, bdWriteSerialised, bd, bdErr, getTasks } from './beads.mjs';
17
18
  import { getMetrics } from './metrics.mjs';
18
19
  import { readVerdicts } from './verdicts.mjs';
@@ -54,18 +55,12 @@ async function dispatch(req, res, url, cwd) {
54
55
  // it MUST reject cross-origin requests. The board listens on 127.0.0.1
55
56
  // but a malicious page the user visits can still issue text/plain POSTs
56
57
  // (simple CORS request — no preflight) to localhost. Two gates:
57
- // 1) Origin / Referer must match http://localhost:PORT or 127.0.0.1:PORT.
58
+ // 1) Origin / Referer must be same-origin (originAllowed() in lib/util.mjs —
59
+ // also enforced as the top-level CSRF guard in server.mjs before dispatch()
60
+ // is ever called, so this is defense-in-depth, not the only check).
58
61
  // 2) Resolved target path must live inside HOME — no /tmp, no /etc.
59
62
  if (pathname === '/api/projects/register' && req.method === 'POST') {
60
- const origin = req.headers.origin || req.headers.referer || '';
61
- const expectedOrigin = `http://localhost:${PORT}`;
62
- const expectedOrigin2 = `http://127.0.0.1:${PORT}`;
63
- const originOk = !origin
64
- || origin === expectedOrigin
65
- || origin === expectedOrigin2
66
- || origin.startsWith(expectedOrigin + '/')
67
- || origin.startsWith(expectedOrigin2 + '/');
68
- if (!originOk) {
63
+ if (!originAllowed(req)) {
69
64
  res.writeHead(403, { 'Content-Type': 'application/json' });
70
65
  res.end(JSON.stringify({ error: 'origin not allowed' }));
71
66
  return true;
@@ -384,8 +379,8 @@ async function dispatch(req, res, url, cwd) {
384
379
  const shareState = getShareState(gateCwd);
385
380
  if (shareState.enabled) {
386
381
  toggleShare(true, gateCwd, true)
387
- .then(() => console.log(`report: auto-republished after gate approve (${id})`))
388
- .catch(e => console.warn(`report: republish after gate failed: ${e.message}`));
382
+ .then(() => log.info(`report: auto-republished after gate approve (${id})`))
383
+ .catch(e => log.warn(`report: republish after gate failed: ${e.message}`));
389
384
  }
390
385
  }
391
386
  });
@@ -850,16 +845,10 @@ async function dispatch(req, res, url, cwd) {
850
845
  // (DESIGN-agents-fleet-view §9 Top-2 #2: reversible sidecar chosen over
851
846
  // filesystem move; founder may revise.)
852
847
  if (pathname.startsWith('/api/agents/') && req.method === 'POST') {
853
- // Cross-origin guard — same pattern as BH-23 on /api/projects/register.
854
- const origin = req.headers.origin || req.headers.referer || '';
855
- const expectedOrigin = `http://localhost:${PORT}`;
856
- const expectedOrigin2 = `http://127.0.0.1:${PORT}`;
857
- const originOk = !origin
858
- || origin === expectedOrigin
859
- || origin === expectedOrigin2
860
- || origin.startsWith(expectedOrigin + '/')
861
- || origin.startsWith(expectedOrigin2 + '/');
862
- if (!originOk) {
848
+ // Cross-origin guard — same helper as BH-23 on /api/projects/register
849
+ // (originAllowed() in lib/util.mjs; also enforced as the top-level CSRF
850
+ // guard in server.mjs before dispatch() is ever called).
851
+ if (!originAllowed(req)) {
863
852
  res.writeHead(403, { 'Content-Type': 'application/json' });
864
853
  res.end(JSON.stringify({ error: 'origin_not_allowed' }));
865
854
  return true;
@@ -1,4 +1,5 @@
1
1
  import fs from 'fs';
2
+ import path from 'path';
2
3
  import { PORT } from './config.mjs';
3
4
 
4
5
  /**
@@ -33,4 +34,16 @@ function readFileSafe(p) {
33
34
  try { return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null; } catch { return null; }
34
35
  }
35
36
 
36
- export { csvCell, originAllowed, eventSurface, readFileSafe };
37
+ // Path-containment check used by any handler that joins user-controlled input
38
+ // onto a base directory (static file serving, doc reads, etc.). Resolves both
39
+ // sides and requires `target` to be exactly `base`, or a real descendant of
40
+ // it (base + path.sep prefix) — this is what actually blocks ".." escapes,
41
+ // since a naive startsWith(base) would wrongly allow a sibling directory
42
+ // like "/public-evil" when base is "/public".
43
+ function isInsideDir(base, target) {
44
+ const resolvedBase = path.resolve(base);
45
+ const resolvedTarget = path.resolve(target);
46
+ return resolvedTarget === resolvedBase || resolvedTarget.startsWith(resolvedBase + path.sep);
47
+ }
48
+
49
+ export { csvCell, originAllowed, eventSurface, readFileSafe, isInsideDir };
@@ -40,17 +40,46 @@ function watchBeads() {
40
40
  }, 200));
41
41
  };
42
42
 
43
+ // Tracks dirs whose interactions.jsonl file watch is already registered,
44
+ // so the late-registration path (b) below doesn't double-watch a file
45
+ // that was already picked up at startup by (a).
46
+ const interactionsWatched = new Set();
47
+
48
+ const watchInteractionsFile = (dir) => {
49
+ if (interactionsWatched.has(dir)) return;
50
+ const interactionsFile = path.join(dir, '.beads', 'interactions.jsonl');
51
+ try {
52
+ fs.watch(interactionsFile, () => schedule(dir));
53
+ interactionsWatched.add(dir);
54
+ } catch {}
55
+ };
56
+
43
57
  for (const dir of dirs) {
44
58
  // (a) interactions.jsonl — captures bd update/close
45
59
  const interactionsFile = path.join(dir, '.beads', 'interactions.jsonl');
46
60
  if (fs.existsSync(interactionsFile)) {
47
- try { fs.watch(interactionsFile, () => schedule(dir)); } catch {}
61
+ watchInteractionsFile(dir);
48
62
  }
49
63
  // (b) dolt embeddeddolt directory (recursive) — captures bd create
50
64
  const doltDir = path.join(dir, '.beads', 'embeddeddolt');
51
65
  if (fs.existsSync(doltDir)) {
52
66
  try { fs.watch(doltDir, { recursive: true }, () => schedule(dir)); } catch {}
53
67
  }
68
+ // (c) .beads dir itself — interactions.jsonl is only registered above if
69
+ // it already exists at startup; if bd creates it later (first `bd
70
+ // update`/`close` on a project with no prior interaction log), that
71
+ // event was previously missed entirely (great_cto-lvai). Watch the
72
+ // .beads dir (non-recursive) and register the file watch as soon as it
73
+ // appears, guarded against double-registration via interactionsWatched.
74
+ const beadsDir = path.join(dir, '.beads');
75
+ if (fs.existsSync(beadsDir)) {
76
+ try {
77
+ fs.watch(beadsDir, (eventType, filename) => {
78
+ if (filename === 'interactions.jsonl') watchInteractionsFile(dir);
79
+ schedule(dir);
80
+ });
81
+ } catch {}
82
+ }
54
83
  }
55
84
  }
56
85
 
@@ -0,0 +1,21 @@
1
+ # Vendored fonts
2
+
3
+ great_cto ships zero telemetry (see docs/PRIVACY.md). The board previously
4
+ loaded Geist / Geist Mono from fonts.googleapis.com + fonts.gstatic.com on
5
+ every open — an IP leak and an offline-breaker. Only the two families the
6
+ CSS actually uses (`--sans`/`--serif` → Geist, `--mono` → Geist Mono; Inter
7
+ and JetBrains Mono in the stack were unused fallback names, not vendored)
8
+ are fetched here and served locally.
9
+
10
+ | File | Family | Source | Subset |
11
+ |------|--------|--------|--------|
12
+ | `geist-variable.woff2` | Geist (variable, wght 400-700) | https://fonts.gstatic.com/s/geist/v5/gyByhwUxId8gMEwcGFWNOITd.woff2 | latin |
13
+ | `geist-mono-variable.woff2` | Geist Mono (variable, wght 400-500) | https://fonts.gstatic.com/s/geistmono/v6/or3nQ6H-1_WfwkMZI_qYFrcdmhHkjko.woff2 | latin |
14
+
15
+ Fetched via the standard `fonts.googleapis.com/css2` request (same URLs
16
+ Google Fonts already serves to browsers); only the `latin` subset was kept
17
+ since the UI is English-only — cyrillic/vietnamese/symbols subsets were
18
+ dropped. Total added weight: ~56 KB. If a font file is ever unreachable at
19
+ build/vendor time, `index.html`'s font stack already falls back to
20
+ `-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` / system-mono
21
+ so the UI still renders — see the `--sans`/`--mono` custom properties.
@@ -0,0 +1,26 @@
1
+ /* Local Geist / Geist Mono @font-face rules (great_cto-kge6).
2
+ * Vendored to replace the fonts.googleapis.com / fonts.gstatic.com CDN
3
+ * request the board used to make on every open — see PROVENANCE.md.
4
+ * Each family ships as a single variable-font woff2 (weight axis 400-700+),
5
+ * so one @font-face declaration with a wide font-weight range covers every
6
+ * weight the page uses (400/500/600/700), matching what the CDN's per-weight
7
+ * CSS actually served (identical bytes for each weight in the latin subset).
8
+ * Only the latin subset is vendored — the UI is English-only; falls back to
9
+ * the system stack (see --sans/--mono in index.html) for anything else.
10
+ */
11
+ @font-face {
12
+ font-family: 'Geist';
13
+ font-style: normal;
14
+ font-weight: 400 700;
15
+ font-display: swap;
16
+ src: url('geist-variable.woff2') format('woff2');
17
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
18
+ }
19
+ @font-face {
20
+ font-family: 'Geist Mono';
21
+ font-style: normal;
22
+ font-weight: 400 500;
23
+ font-display: swap;
24
+ src: url('geist-mono-variable.woff2') format('woff2');
25
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
26
+ }
@@ -0,0 +1,21 @@
1
+ # Vendored third-party assets
2
+
3
+ great_cto ships zero telemetry and phones home to nothing (see docs/PRIVACY.md).
4
+ These files were previously loaded from external CDNs on every board open
5
+ (fonts.googleapis.com, cdn.jsdelivr.net) — an IP leak and an offline-breaker.
6
+ They're now vendored as static assets and served locally instead.
7
+
8
+ ## JS libraries
9
+
10
+ | File | Package | Version | Source | SHA-384 (matches original SRI hash) |
11
+ |------|---------|---------|--------|--------------------------------------|
12
+ | `marked.min.js` | [marked](https://www.npmjs.com/package/marked) | 14.1.3 | https://cdn.jsdelivr.net/npm/marked@14.1.3/marked.min.js | `k8o8HikHweyzW55Wd3wl18ovJj6vHVYNQeQbeSM0fxx+0WiH4TcccOG9uz8Xd2JR` |
13
+ | `purify.min.js` | [dompurify](https://www.npmjs.com/package/dompurify) | 3.1.7 | https://cdn.jsdelivr.net/npm/dompurify@3.1.7/dist/purify.min.js | `XQqX/4yiUGu+oyr87jvWzRuqBUK/adrY0DunhL+tID9m/9dwSpV8h9Fk/Sg6ifVQ` |
14
+
15
+ Downloaded verbatim; hashes verified to match the SRI `integrity` attributes
16
+ the board previously used on the CDN `<script>` tags. These are static
17
+ vendored assets, not npm runtime dependencies — no package.json entry.
18
+
19
+ ## Fonts
20
+
21
+ See `../fonts/PROVENANCE.md` for the vendored Geist / Geist Mono woff2 files.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * marked v14.1.3 - a markdown parser
3
+ * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed)
4
+ * https://github.com/markedjs/marked
5
+ */
6
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/(^|[^\[])\^/g;function p(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(h,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function u(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch{return null}return e}const k={exec:()=>null};function g(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(/\\\|/g,"|");return n}function f(e,t,n){const s=e.length;if(0===s)return"";let r=0;for(;r<s;){const i=e.charAt(s-r-1);if(i!==t||n){if(i===t||!n)break;r++}else r++}return e.slice(0,s-r)}function d(e,t,n,s){const r=t.href,i=t.title?c(t.title):null,l=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:n,href:r,title:i,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,e}return{type:"image",raw:n,href:r,title:i,text:c(l)}}class x{options;rules;lexer;constructor(t){this.options=t||e.defaults}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^(?: {1,4}| {0,3}\t)/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:f(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=f(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:f(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=f(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l<e.length;l++)if(/^ {0,3}>/.test(e[l]))i.push(e[l]),t=!0;else{if(t)break;i.push(e[l])}e=e.slice(l);const o=i.join("\n"),a=o.replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1").replace(/^ {0,3}>[ \t]?/gm,"");n=n?`${n}\n${o}`:o,s=s?`${s}\n${a}`:a;const c=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(a,r,!0),this.lexer.state.top=c,0===e.length)break;const h=r[r.length-1];if("code"===h?.type)break;if("blockquote"===h?.type){const t=h,i=t.raw+"\n"+e.join("\n"),l=this.blockquote(i);r[r.length-1]=l,n=n.substring(0,n.length-t.raw.length)+l.raw,s=s.substring(0,s.length-t.text.length)+l.text;break}if("list"!==h?.type);else{const t=h,i=t.raw+"\n"+e.join("\n"),l=this.list(i);r[r.length-1]=l,n=n.substring(0,n.length-h.raw.length)+l.raw,s=s.substring(0,s.length-t.raw.length)+l.raw,e=i.substring(r[r.length-1].raw.length).split("\n")}}return{type:"blockquote",raw:n,tokens:r,text:s}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(/[^ ]/),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&/^[ \t]*$/.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,p-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,p-1)}}#`),l=new RegExp(`^ {0,${Math.min(3,p-1)}}<[a-z].*>`,"i");for(;e;){const u=e.split("\n",1)[0];let k;if(c=u,this.options.pedantic?(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," "),k=c):k=c.replace(/\t/g," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(k.search(/[^ ]/)>=p||!c.trim())o+="\n"+k.slice(p);else{if(h)break;if(a.replace(/\t/g," ").search(/[^ ]/)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=k.slice(p)}}r.loose||(l?r.loose=!0:/\n[ \t]*\n[ \t]*$/.test(s)&&(l=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:s,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}r.items[r.items.length-1].raw=r.items[r.items.length-1].raw.trimEnd(),r.items[r.items.length-1].text=r.items[r.items.length-1].text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e<r.items.length;e++)if(this.lexer.state.top=!1,r.items[e].tokens=this.lexer.blockTokens(r.items[e].text,[]),!r.loose){const t=r.items[e].tokens.filter((e=>"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e<r.items.length;e++)r.items[e].loose=!0;return r}}html(e){const t=this.rules.block.html.exec(e);if(t){return{type:"html",block:!0,raw:t[0],pre:"pre"===t[1]||"script"===t[1]||"style"===t[1],text:t[0]}}}def(e){const t=this.rules.block.def.exec(e);if(t){const e=t[1].toLowerCase().replace(/\s+/g," "),n=t[2]?t[2].replace(/^<(.*)>$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=g(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(let e=0;e<n.length;e++)i.header.push({text:n[e],tokens:this.lexer.inline(n[e]),header:!0,align:i.align[e]});for(const e of r)i.rows.push(g(e,i.header.length).map(((e,t)=>({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^<a /i.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&/^<\/a>/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^</.test(e)){if(!/>$/.test(e))return;const t=f(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s<e.length;s++)if("\\"===e[s])s++;else if(e[s]===t[0])n++;else if(e[s]===t[1]&&(n--,n<0))return s;return-1}(t[2],"()");if(e>-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^</.test(n)&&(n=this.options.pedantic&&!/>$/.test(e)?n.slice(1):n.slice(1,-1)),d(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return d(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const b=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,w=/(?:[*+-]|\d{1,9}[.)])/,m=p(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,w).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),y=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,$=/(?!\s*\])(?:\\.|[^\[\]\\])+/,z=p(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",$).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),T=p(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,w).getRegex(),R="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",_=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,A=p("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",_).replace("tag",R).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),S=p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),I={blockquote:p(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",S).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:z,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:b,html:A,lheading:m,list:T,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:S,table:k,text:/^[^\n]+/},E=p("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),q={...I,table:E,paragraph:p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",E).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex()},Z={...I,html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:\"[^\"]*\"|'[^']*'|\\s[^'\"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",_).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:k,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:p(y).replace("hr",b).replace("heading"," *#{1,6} *[^\n]").replace("lheading",m).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},P=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,L=/^( {2,}|\\)\n(?!\s*$)/,v="\\p{P}\\p{S}",Q=p(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,v).getRegex(),B=p(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,v).getRegex(),M=p("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,v).getRegex(),O=p("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,v).getRegex(),j=p(/\\([punct])/,"gu").replace(/punct/g,v).getRegex(),D=p(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),C=p(_).replace("(?:--\x3e|$)","--\x3e").getRegex(),H=p("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",C).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),U=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,X=p(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",U).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),F=p(/^!?\[(label)\]\[(ref)\]/).replace("label",U).replace("ref",$).getRegex(),N=p(/^!?\[(ref)\](?:\[\])?/).replace("ref",$).getRegex(),G={_backpedal:k,anyPunctuation:j,autolink:D,blockSkip:/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,br:L,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:k,emStrongLDelim:B,emStrongRDelimAst:M,emStrongRDelimUnd:O,escape:P,link:X,nolink:N,punctuation:Q,reflink:F,reflinkSearch:p("reflink|nolink(?!\\()","g").replace("reflink",F).replace("nolink",N).getRegex(),tag:H,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,url:k},J={...G,link:p(/^!?\[(label)\]\((.*?)\)/).replace("label",U).getRegex(),reflink:p(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",U).getRegex()},K={...G,escape:p(P).replace("])","~|])").getRegex(),url:p(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},V={...K,br:p(L).replace("{2,}","*").getRegex(),text:p(K.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},W={normal:I,gfm:q,pedantic:Z},Y={normal:G,gfm:K,breaks:V,pedantic:J};class ee{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new x,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={block:W.normal,inline:Y.normal};this.options.pedantic?(n.block=W.pedantic,n.inline=Y.pedantic):this.options.gfm&&(n.block=W.gfm,this.options.breaks?n.inline=Y.breaks:n.inline=Y.gfm),this.tokenizer.rules=n}static get rules(){return{block:W,inline:Y}}static lex(e,t){return new ee(t).lex(e)}static lexInline(e,t){return new ee(t).inlineTokens(e)}lex(e){e=e.replace(/\r\n|\r/g,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e<this.inlineQueue.length;e++){const t=this.inlineQueue[e];this.inlineTokens(t.src,t.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){let s,r,i;for(this.options.pedantic&&(e=e.replace(/\t/g," ").replace(/^ +$/gm,""));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0)))))if(s=this.tokenizer.space(e))e=e.substring(s.raw.length),1===s.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(s);else if(s=this.tokenizer.code(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?t.push(s):(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.fences(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.heading(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.hr(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.blockquote(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.list(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.html(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.def(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title}):(r.raw+="\n"+s.raw,r.text+="\n"+s.raw,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.table(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.lheading(e))e=e.substring(s.raw.length),t.push(s);else{if(i=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(i=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i)))r=t[t.length-1],n&&"paragraph"===r?.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);else if(s=this.tokenizer.text(e))e=e.substring(s.raw.length),r=t[t.length-1],r&&"text"===r.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class te{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(/^\S*/)?.[0],r=e.replace(/\n$/,"")+"\n";return s?'<pre><code class="language-'+c(s)+'">'+(n?r:c(r,!0))+"</code></pre>\n":"<pre><code>"+(n?r:c(r,!0))+"</code></pre>\n"}blockquote({tokens:e}){return`<blockquote>\n${this.parser.parse(e)}</blockquote>\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>\n`}hr(e){return"<hr>\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t<e.items.length;t++){const n=e.items[t];s+=this.listitem(n)}const r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+s+"</"+r+">\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?e.tokens.length>0&&"paragraph"===e.tokens[0].type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+e.tokens[0].tokens[0].text)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" "}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>\n`}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>\n`}table(e){let t="",n="";for(let t=0;t<e.header.length;t++)n+=this.tablecell(e.header[t]);t+=this.tablerow({text:n});let s="";for(let t=0;t<e.rows.length;t++){const r=e.rows[t];n="";for(let e=0;e<r.length;e++)n+=this.tablecell(r[e]);s+=this.tablerow({text:n})}return s&&(s=`<tbody>${s}</tbody>`),"<table>\n<thead>\n"+t+"</thead>\n"+s+"</table>\n"}tablerow({text:e}){return`<tr>\n${e}</tr>\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>\n`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${e}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=u(e);if(null===r)return s;let i='<a href="'+(e=r)+'"';return t&&(i+=' title="'+t+'"'),i+=">"+s+"</a>",i}image({href:e,title:t,text:n}){const s=u(e);if(null===s)return n;let r=`<img src="${e=s}" alt="${n}"`;return t&&(r+=` title="${t}"`),r+=">",r}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):e.text}}class ne{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}}class se{options;renderer;textRenderer;constructor(t){this.options=t||e.defaults,this.options.renderer=this.options.renderer||new te,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new ne}static parse(e,t){return new se(t).parse(e)}static parseInline(e,t){return new se(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s<e.length;s++){const r=e[s];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const e=r,t=this.options.extensions.renderers[e.type].call({parser:this},e);if(!1!==t||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(e.type)){n+=t||"";continue}}const i=r;switch(i.type){case"space":n+=this.renderer.space(i);continue;case"hr":n+=this.renderer.hr(i);continue;case"heading":n+=this.renderer.heading(i);continue;case"code":n+=this.renderer.code(i);continue;case"table":n+=this.renderer.table(i);continue;case"blockquote":n+=this.renderer.blockquote(i);continue;case"list":n+=this.renderer.list(i);continue;case"html":n+=this.renderer.html(i);continue;case"paragraph":n+=this.renderer.paragraph(i);continue;case"text":{let r=i,l=this.renderer.text(r);for(;s+1<e.length&&"text"===e[s+1].type;)r=e[++s],l+="\n"+this.renderer.text(r);n+=t?this.renderer.paragraph({type:"paragraph",raw:l,text:l,tokens:[{type:"text",raw:l,text:l}]}):l;continue}default:{const e='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(e),"";throw new Error(e)}}}return n}parseInline(e,t){t=t||this.renderer;let n="";for(let s=0;s<e.length;s++){const r=e[s];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const e=this.options.extensions.renderers[r.type].call({parser:this},r);if(!1!==e||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(r.type)){n+=e||"";continue}}const i=r;switch(i.type){case"escape":case"text":n+=t.text(i);break;case"html":n+=t.html(i);break;case"link":n+=t.link(i);break;case"image":n+=t.image(i);break;case"strong":n+=t.strong(i);break;case"em":n+=t.em(i);break;case"codespan":n+=t.codespan(i);break;case"br":n+=t.br(i);break;case"del":n+=t.del(i);break;default:{const e='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(e),"";throw new Error(e)}}}return n}}class re{options;block;constructor(t){this.options=t||e.defaults}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}provideLexer(){return this.block?ee.lex:ee.lexInline}provideParser(){return this.block?se.parse:se.parseInline}}class ie{defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=se;Renderer=te;TextRenderer=ne;Lexer=ee;Tokenizer=x;Hooks=re;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(const s of e)switch(n=n.concat(t.call(this,s)),s.type){case"table":{const e=s;for(const s of e.header)n=n.concat(this.walkTokens(s.tokens,t));for(const s of e.rows)for(const e of s)n=n.concat(this.walkTokens(e.tokens,t));break}case"list":{const e=s;n=n.concat(this.walkTokens(e.items,t));break}default:{const e=s;this.defaults.extensions?.childTokens?.[e.type]?this.defaults.extensions.childTokens[e.type].forEach((s=>{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new te(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new x(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new re;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];re.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ee.lex(e,t??this.defaults)}parser(e,t){return se.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?ee.lex:ee.lexInline,o=r.hooks?r.hooks.provideParser():e?se.parse:se.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="<p>An error occurred:</p><pre>"+c(n.message+"",!0)+"</pre>";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const le=new ie;function oe(e,t){return le.parse(e,t)}oe.options=oe.setOptions=function(e){return le.setOptions(e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.getDefaults=t,oe.defaults=e.defaults,oe.use=function(...e){return le.use(...e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.walkTokens=function(e,t){return le.walkTokens(e,t)},oe.parseInline=le.parseInline,oe.Parser=se,oe.parser=se.parse,oe.Renderer=te,oe.TextRenderer=ne,oe.Lexer=ee,oe.lexer=ee.lex,oe.Tokenizer=x,oe.Hooks=re,oe.parse=oe;const ae=oe.options,ce=oe.setOptions,he=oe.use,pe=oe.walkTokens,ue=oe.parseInline,ke=oe,ge=se.parse,fe=ee.lex;e.Hooks=re,e.Lexer=ee,e.Marked=ie,e.Parser=se,e.Renderer=te,e.TextRenderer=ne,e.Tokenizer=x,e.getDefaults=t,e.lexer=fe,e.marked=oe,e.options=ae,e.parse=ke,e.parseInline=ue,e.parser=ge,e.setOptions=ce,e.use=he,e.walkTokens=pe}));
@@ -0,0 +1,3 @@
1
+ /*! @license DOMPurify 3.1.7 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.7/LICENSE */
2
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),_=b(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(N,t)});var N;function b(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function S(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t<e.length;t++){E(e,t)||(e[t]=null)}return e}function w(t){const n=l(null);for(const[o,r]of e(t)){E(t,o)&&(Array.isArray(r)?n[o]=R(r):r&&"object"==typeof r&&r.constructor===Object?n[o]=w(r):n[o]=r)}return n}function C(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return b(n.get);if("function"==typeof n.value)return b(n.value)}e=o(e)}return function(){return null}}const L=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),v=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),D=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),O=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),x=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),k=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),M=i(["#text"]),I=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),U=i(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),P=i(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),F=i(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),H=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),z=a(/<%[\w\W]*|[\w\W]*%>/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=1,Z=3,J=7,Q=8,ee=9,te=function(){return"undefined"==typeof window?null:window};var ne=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:te();const o=e=>t(e);if(o.version="3.1.7",o.removed=[],!n||!n.document||n.document.nodeType!==ee)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=C(j,"cloneNode"),$=C(j,"remove"),ne=C(j,"nextSibling"),oe=C(j,"childNodes"),re=C(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ie,ae="";const{implementation:le,createNodeIterator:ce,createDocumentFragment:se,getElementsByTagName:ue}=r,{importNode:me}=a;let pe={};o.isSupported="function"==typeof e&&"function"==typeof re&&le&&void 0!==le.createHTMLDocument;const{MUSTACHE_EXPR:fe,ERB_EXPR:de,TMPLIT_EXPR:he,DATA_ATTR:ge,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:_e}=K;let{IS_ALLOWED_URI:Ae}=K,Ne=null;const be=S({},[...L,...v,...D,...x,...M]);let Se=null;const Re=S({},[...I,...U,...P,...F]);let we=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ce=null,Le=null,ve=!0,De=!0,Oe=!1,xe=!0,ke=!1,Me=!0,Ie=!1,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!0,We=!1,Ge=!0,Ye=!1,je={},Xe=null;const qe=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let $e=null;const Ke=S({},["audio","video","img","source","image","track"]);let Ve=null;const Ze=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Je="http://www.w3.org/1998/Math/MathML",Qe="http://www.w3.org/2000/svg",et="http://www.w3.org/1999/xhtml";let tt=et,nt=!1,ot=null;const rt=S({},[Je,Qe,et],d);let it=null;const at=["application/xhtml+xml","text/html"];let lt=null,ct=null;const st=r.createElement("form"),ut=function(e){return e instanceof RegExp||e instanceof Function},mt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ct||ct!==e){if(e&&"object"==typeof e||(e={}),e=w(e),it=-1===at.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,lt="application/xhtml+xml"===it?d:f,Ne=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,lt):be,Se=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,lt):Re,ot=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):rt,Ve=E(e,"ADD_URI_SAFE_ATTR")?S(w(Ze),e.ADD_URI_SAFE_ATTR,lt):Ze,$e=E(e,"ADD_DATA_URI_TAGS")?S(w(Ke),e.ADD_DATA_URI_TAGS,lt):Ke,Xe=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,lt):qe,Ce=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,lt):{},Le=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,lt):{},je=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,ve=!1!==e.ALLOW_ARIA_ATTR,De=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,xe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Me=!1!==e.SAFE_FOR_XML,Ie=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,He=e.RETURN_DOM_FRAGMENT||!1,ze=e.RETURN_TRUSTED_TYPE||!1,Pe=e.FORCE_BODY||!1,Be=!1!==e.SANITIZE_DOM,We=e.SANITIZE_NAMED_PROPS||!1,Ge=!1!==e.KEEP_CONTENT,Ye=e.IN_PLACE||!1,Ae=e.ALLOWED_URI_REGEXP||Y,tt=e.NAMESPACE||et,we=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ut(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ut(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(De=!1),He&&(Fe=!0),je&&(Ne=S({},M),Se=[],!0===je.html&&(S(Ne,L),S(Se,I)),!0===je.svg&&(S(Ne,v),S(Se,U),S(Se,F)),!0===je.svgFilters&&(S(Ne,D),S(Se,U),S(Se,F)),!0===je.mathMl&&(S(Ne,x),S(Se,P),S(Se,F))),e.ADD_TAGS&&(Ne===be&&(Ne=w(Ne)),S(Ne,e.ADD_TAGS,lt)),e.ADD_ATTR&&(Se===Re&&(Se=w(Se)),S(Se,e.ADD_ATTR,lt)),e.ADD_URI_SAFE_ATTR&&S(Ve,e.ADD_URI_SAFE_ATTR,lt),e.FORBID_CONTENTS&&(Xe===qe&&(Xe=w(Xe)),S(Xe,e.FORBID_CONTENTS,lt)),Ge&&(Ne["#text"]=!0),Ie&&S(Ne,["html","head","body"]),Ne.table&&(S(Ne,["tbody"]),delete Ce.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ie=e.TRUSTED_TYPES_POLICY,ae=ie.createHTML("")}else void 0===ie&&(ie=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(G,c)),null!==ie&&"string"==typeof ae&&(ae=ie.createHTML(""));i&&i(e),ct=e}},pt=S({},["mi","mo","mn","ms","mtext"]),ft=S({},["annotation-xml"]),dt=S({},["title","style","font","a","script"]),ht=S({},[...v,...D,...O]),gt=S({},[...x,...k]),Tt=function(e){p(o.removed,{element:e});try{re(e).removeChild(e)}catch(t){$(e)}},yt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Se[e])if(Fe||He)try{Tt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Et=function(e){let t=null,n=null;if(Pe)e="<remove></remove>"+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===it&&tt===et&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=ie?ie.createHTML(e):e;if(tt===et)try{t=(new W).parseFromString(o,it)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(tt,"template",null);try{t.documentElement.innerHTML=nt?ae:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),tt===et?ue.call(t,Ie?"html":"body")[0]:Ie?t.documentElement:i},_t=function(e){return ce.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},At=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof b&&e instanceof b},bt=function(e,t,n){pe[e]&&u(pe[e],(e=>{e.call(o,t,n,ct)}))},St=function(e){let t=null;if(bt("beforeSanitizeElements",e,null),At(e))return Tt(e),!0;const n=lt(e.nodeName);if(bt("uponSanitizeElement",e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Nt(e.firstElementChild)&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent))return Tt(e),!0;if(e.nodeType===J)return Tt(e),!0;if(Me&&e.nodeType===Q&&_(/<[/\w]/g,e.data))return Tt(e),!0;if(!Ne[n]||Ce[n]){if(!Ce[n]&&wt(n)){if(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n))return!1;if(we.tagNameCheck instanceof Function&&we.tagNameCheck(n))return!1}if(Ge&&!Xe[n]){const t=re(e)||e.parentNode,n=oe(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=X(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,ne(e))}}}return Tt(e),!0}return e instanceof R&&!function(e){let t=re(e);t&&t.tagName||(t={namespaceURI:tt,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!ot[e.namespaceURI]&&(e.namespaceURI===Qe?t.namespaceURI===et?"svg"===n:t.namespaceURI===Je?"svg"===n&&("annotation-xml"===o||pt[o]):Boolean(ht[n]):e.namespaceURI===Je?t.namespaceURI===et?"math"===n:t.namespaceURI===Qe?"math"===n&&ft[o]:Boolean(gt[n]):e.namespaceURI===et?!(t.namespaceURI===Qe&&!ft[o])&&!(t.namespaceURI===Je&&!pt[o])&&!gt[n]&&(dt[n]||!ht[n]):!("application/xhtml+xml"!==it||!ot[e.namespaceURI]))}(e)?(Tt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!_(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Z&&(t=e.textContent,u([fe,de,he],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),bt("afterSanitizeElements",e,null),!1):(Tt(e),!0)},Rt=function(e,t,n){if(Be&&("id"===t||"name"===t)&&(n in r||n in st))return!1;if(De&&!Le[t]&&_(ge,t));else if(ve&&_(Te,t));else if(!Se[t]||Le[t]){if(!(wt(e)&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&_(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ve[t]);else if(_(Ae,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!$e[e]){if(Oe&&!_(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},wt=function(e){return"annotation-xml"!==e&&h(e,_e)},Ct=function(e){bt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Se};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=lt(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,bt("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(yt(a,e),!n.keepAttr)continue;if(!xe&&_(/\/>/i,p)){yt(a,e);continue}ke&&u([fe,de,he],(e=>{p=g(p,e," ")}));const f=lt(e.nodeName);if(Rt(f,s,p))if(!We||"id"!==s&&"name"!==s||(yt(a,e),p="user-content-"+p),Me&&_(/((--!?|])>)|<\/(style|title)/i,p))yt(a,e);else{if(ie&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ie.createHTML(p);break;case"TrustedScriptURL":p=ie.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),At(e)?Tt(e):m(o.removed)}catch(e){}}}bt("afterSanitizeAttributes",e,null)},Lt=function e(t){let n=null;const o=_t(t);for(bt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)bt("uponSanitizeShadowNode",n,null),St(n)||(n.content instanceof s&&e(n.content),Ct(n));bt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(nt=!e,nt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Ue||mt(t),o.removed=[],"string"==typeof e&&(Ye=!1),Ye){if(e.nodeName){const t=lt(e.nodeName);if(!Ne[t]||Ce[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Et("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===V&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!Ie&&-1===e.indexOf("<"))return ie&&ze?ie.createHTML(e):e;if(n=Et(e),!n)return Fe?null:ze?ae:""}n&&Pe&&Tt(n.firstChild);const c=_t(Ye?e:n);for(;i=c.nextNode();)St(i)||(i.content instanceof s&&Lt(i.content),Ct(i));if(Ye)return e;if(Fe){if(He)for(l=se.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Se.shadowroot||Se.shadowrootmode)&&(l=me.call(a,l,!0)),l}let m=Ie?n.outerHTML:n.innerHTML;return Ie&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&_(q,n.ownerDocument.doctype.name)&&(m="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+m),ke&&u([fe,de,he],(e=>{m=g(m,e," ")})),ie&&ze?ie.createHTML(m):m},o.setConfig=function(){mt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ue=!0},o.clearConfig=function(){ct=null,Ue=!1},o.isValidAttribute=function(e,t,n){ct||mt({});const o=lt(e),r=lt(t);return Rt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(pe[e]=pe[e]||[],p(pe[e],t))},o.removeHook=function(e){if(pe[e])return m(pe[e])},o.removeHooks=function(e){pe[e]&&(pe[e]=[])},o.removeAllHooks=function(){pe={}},o}();return ne}));
3
+ //# sourceMappingURL=purify.min.js.map
@@ -26,11 +26,9 @@
26
26
  <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32.png" />
27
27
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16.png" />
28
28
  <link rel="shortcut icon" href="/assets/favicon.ico" />
29
- <link rel="preconnect" href="https://fonts.googleapis.com" />
30
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
31
- <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
32
- <script src="https://cdn.jsdelivr.net/npm/marked@14.1.3/marked.min.js" integrity="sha384-k8o8HikHweyzW55Wd3wl18ovJj6vHVYNQeQbeSM0fxx+0WiH4TcccOG9uz8Xd2JR" crossorigin="anonymous"></script>
33
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.7/dist/purify.min.js" integrity="sha384-XQqX/4yiUGu+oyr87jvWzRuqBUK/adrY0DunhL+tID9m/9dwSpV8h9Fk/Sg6ifVQ" crossorigin="anonymous"></script>
29
+ <link href="/assets/fonts/fonts.css" rel="stylesheet" />
30
+ <script src="/assets/vendor/marked.min.js"></script>
31
+ <script src="/assets/vendor/purify.min.js"></script>
34
32
  <style>
35
33
  :root {
36
34
  /* Dark emerald — matches landing greatcto.systems */
@@ -3694,8 +3692,7 @@ function connectSSE() {
3694
3692
  if (window._gctoSSE) { try { window._gctoSSE.close(); } catch {} }
3695
3693
  const es = new EventSource(`/api/sse${pqs()}`);
3696
3694
  window._gctoSSE = es;
3697
- es.addEventListener('tasks', e => {
3698
- const newTasks = JSON.parse(e.data);
3695
+ const applyTasksSnapshot = (newTasks) => {
3699
3696
  // Detect status transitions for toast feed
3700
3697
  if (allTasks.length) {
3701
3698
  const prevById = new Map(allTasks.map(t => [t.id, t]));
@@ -3713,6 +3710,21 @@ function connectSSE() {
3713
3710
  updateNavCounts();
3714
3711
  try { renderKanban(filterTasks(searchQuery)); } catch (e) { console.warn('SSE renderKanban failed', e); }
3715
3712
  refreshInbox();
3713
+ };
3714
+ es.addEventListener('tasks', e => {
3715
+ const newTasks = JSON.parse(e.data);
3716
+ // A transient bd failure (dolt lock, timeout) can produce an empty
3717
+ // snapshot indistinguishable from "no tasks" (great_cto-e2ew). Don't
3718
+ // blindly wipe a populated board — refetch once via the regular API
3719
+ // (which applies the same last-good-cache guard server-side) and only
3720
+ // accept the empty state if the refetch confirms it.
3721
+ if (newTasks.length === 0 && allTasks.length > 0) {
3722
+ api(`/api/tasks${pqs()}`).then(refetched => {
3723
+ applyTasksSnapshot(refetched || []);
3724
+ });
3725
+ return;
3726
+ }
3727
+ applyTasksSnapshot(newTasks);
3716
3728
  });
3717
3729
  es.addEventListener('pipeline', e => {
3718
3730
  try {
@@ -7,9 +7,13 @@
7
7
  <title>{{PROJECT}} · Engineering Report · {{DATE}}</title>
8
8
  <meta property="og:title" content="{{PROJECT}} · Engineering Report" />
9
9
  <meta property="og:description" content="AI agents shipped features, saved time and cost. See the breakdown." />
10
- <link rel="preconnect" href="https://fonts.googleapis.com" />
11
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
- <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ <!-- great_cto-kge6: this page is published to an external host
11
+ (greatcto.systems/r/<hash>), not served from the local board — it can't
12
+ reference /assets/fonts locally-vendored files. Dropped the Google Fonts
13
+ CDN call entirely; the --sans/--serif/--mono stacks below already fall
14
+ back to system fonts (-apple-system, BlinkMacSystemFont, etc.) when
15
+ "Geist"/"Geist Mono" aren't available, so this renders cleanly with zero
16
+ external calls instead of leaking the viewer's IP to Google. -->
13
17
  <style>
14
18
  :root {
15
19
  /* Dark emerald — matches landing greatcto.systems */
@@ -13,11 +13,12 @@ import fs from 'fs';
13
13
  import path from 'path';
14
14
  import { spawnSync } from 'child_process';
15
15
  import { PORT, PUBLIC, HOST } from './lib/config.mjs';
16
- import { originAllowed } from './lib/util.mjs';
16
+ import { originAllowed, isInsideDir } from './lib/util.mjs';
17
17
  import { discoverProjects, resolveProjectInfo } from './lib/projects.mjs';
18
18
  import { startAlertCron } from './lib/alerts.mjs';
19
19
  import { watchBeads, watchVerdicts } from './lib/watchers.mjs';
20
20
  import { dispatch } from './lib/routes.mjs';
21
+ import { log } from './lib/log.mjs';
21
22
 
22
23
  // ── Helpers ────────────────────────────────────────────────────────────────────
23
24
 
@@ -77,7 +78,13 @@ const server = http.createServer(async (req, res) => {
77
78
  // Static files
78
79
  let filePath = pathname === '/' ? '/index.html' : pathname;
79
80
  const fullPath = path.join(PUBLIC, filePath);
80
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
81
+ // Path-traversal guard (mirrors the /api/doc containment check in lib/routes.mjs):
82
+ // the joined path must resolve to somewhere inside PUBLIC. Without this, a decoded
83
+ // pathname like /..%2F..%2Fetc%2Fpasswd can escape the public/ directory via
84
+ // path.join's ".." collapsing. Reject with 404 (same as the generic static 404
85
+ // below) so a traversal attempt is indistinguishable from a missing file — no
86
+ // signal to a prober about what exists outside PUBLIC.
87
+ if (isInsideDir(PUBLIC, fullPath) && fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
81
88
  const ext = path.extname(fullPath);
82
89
  const mime = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.svg': 'image/svg+xml' };
83
90
  // HTML must never cache — board UI is iterated daily and stale layouts
@@ -102,15 +109,15 @@ const server = http.createServer(async (req, res) => {
102
109
 
103
110
  // ── Start ──────────────────────────────────────────────────────────────────────
104
111
  server.listen(PORT, HOST, () => {
105
- console.log(`great_cto board → http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`);
112
+ log.info(`great_cto board → http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`);
106
113
  if (HOST !== '127.0.0.1' && HOST !== 'localhost') {
107
- console.log(` ⚠ bound to ${HOST} — reachable beyond this machine. Operators authenticate via invite`);
108
- console.log(` links; put your reverse-proxy auth in front for anything admin-grade.`);
114
+ log.info(` ⚠ bound to ${HOST} — reachable beyond this machine. Operators authenticate via invite`);
115
+ log.info(` links; put your reverse-proxy auth in front for anything admin-grade.`);
109
116
  }
110
117
  // Discover all great_cto projects on disk asynchronously — don't block
111
118
  // the listening event so /api/tasks is available immediately.
112
119
  discoverProjects().then(n => {
113
- if (n > 0) console.log(` → discovered ${n} project${n === 1 ? '' : 's'} with .great_cto/PROJECT.md`);
120
+ if (n > 0) log.info(` → discovered ${n} project${n === 1 ? '' : 's'} with .great_cto/PROJECT.md`);
114
121
  }).catch(() => {}); // non-fatal
115
122
  watchBeads();
116
123
  startAlertCron();
@@ -125,9 +132,9 @@ server.listen(PORT, HOST, () => {
125
132
 
126
133
  server.on('error', e => {
127
134
  if (e.code === 'EADDRINUSE') {
128
- console.log(`Port ${PORT} in use — board already running at http://localhost:${PORT}`);
135
+ log.info(`Port ${PORT} in use — board already running at http://localhost:${PORT}`);
129
136
  } else {
130
- console.error(e);
137
+ log.error(e);
131
138
  }
132
139
  process.exit(0);
133
140
  });
package/dist/telemetry.js CHANGED
@@ -27,9 +27,9 @@ import * as path from "node:path";
27
27
  import * as os from "node:os";
28
28
  import * as crypto from "node:crypto";
29
29
  const TELEMETRY_ENDPOINT = process.env.GREAT_CTO_TELEMETRY_ENDPOINT
30
- || "https://great-cto-telemetry.alexander-velikiy.workers.dev/v1/event";
31
- // Note: workers.dev URL is the temporary default until telemetry.greatcto.systems
32
- // custom domain is bound. Override anytime with GREAT_CTO_TELEMETRY_ENDPOINT.
30
+ || "https://telemetry.greatcto.systems/v1/event";
31
+ // Override anytime with GREAT_CTO_TELEMETRY_ENDPOINT (e.g. to point at a
32
+ // workers.dev URL during local worker development).
33
33
  const TELEMETRY_TIMEOUT_MS = 1000;
34
34
  // Allowlist — anything else is dropped client-side and server-side.
35
35
  const ALLOWED_COMMANDS = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.77.1",
3
+ "version": "2.78.0",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",