austere-axi 0.3.0 → 0.4.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.
package/dist/cli.mjs CHANGED
@@ -608,7 +608,7 @@ var init_paths = __esm({
608
608
  HERE = path.dirname(fileURLToPath(import.meta.url));
609
609
  BUNDLED = true;
610
610
  ASSET_DIR = BUNDLED ? HERE : path.resolve(HERE, "../../dist");
611
- VERSION = true ? "0.3.0" : JSON.parse(fs.readFileSync(path.resolve(HERE, "../../package.json"), "utf8")).version;
611
+ VERSION = true ? "0.4.0" : JSON.parse(fs.readFileSync(path.resolve(HERE, "../../package.json"), "utf8")).version;
612
612
  CLI_ENTRY = BUNDLED ? fileURLToPath(import.meta.url) : path.resolve(HERE, "main.js");
613
613
  }
614
614
  });
@@ -650,7 +650,10 @@ var init_packs_meta = __esm({
650
650
  });
651
651
 
652
652
  // src/shared/catalog.js
653
- function searchCatalog(entries, query, { notesOf = () => [], limit = 8 } = {}) {
653
+ function searchCatalog(entries, query, { notesOf = (
654
+ /** @type {(entry: any) => string[]} */
655
+ (() => [])
656
+ ), limit = 8 } = {}) {
654
657
  const q2 = String(query || "").trim().toLowerCase();
655
658
  const terms = words(q2);
656
659
  if (!terms.length) return [];
@@ -871,12 +874,6 @@ function setHubEnabled(enabled) {
871
874
  function hubDbFile() {
872
875
  return path3.join(stateDir(), "hub.db");
873
876
  }
874
- function hubUnsupported(version2 = process.versions.node) {
875
- const [a, b] = version2.split(".").map(Number);
876
- const [x, y] = HUB_MIN_NODE.split(".").map(Number);
877
- if (a > x || a === x && b >= y) return null;
878
- return `the hub needs Node ${HUB_MIN_NODE} or newer (for node:sqlite); this is ${version2}`;
879
- }
880
877
  async function loadSqlite() {
881
878
  if (DatabaseSync) return DatabaseSync;
882
879
  const emit2 = process.emitWarning;
@@ -901,6 +898,8 @@ function projectOf(file2) {
901
898
  }
902
899
  }
903
900
  function itemStatus(entry, pending) {
901
+ if (entry.discarded) return "discarded";
902
+ if (entry.held) return "held";
904
903
  if (pending) return "open";
905
904
  if (entry.doneAt) return "addressed";
906
905
  return "received";
@@ -912,7 +911,7 @@ function itemText(it2) {
912
911
  return it2.text ?? it2.note ?? "";
913
912
  }
914
913
  async function registerWithHub(file2, title, id) {
915
- if (!hubEnabled() || hubUnsupported()) return;
914
+ if (!hubEnabled()) return;
916
915
  try {
917
916
  const hub = await Hub.open();
918
917
  try {
@@ -923,11 +922,10 @@ async function registerWithHub(file2, title, id) {
923
922
  } catch {
924
923
  }
925
924
  }
926
- var HUB_MIN_NODE, DatabaseSync, SCHEMA, Hub;
925
+ var DatabaseSync, SCHEMA, Hub;
927
926
  var init_hub = __esm({
928
927
  "src/cli/hub.js"() {
929
928
  init_paths();
930
- HUB_MIN_NODE = "22.13.0";
931
929
  DatabaseSync = null;
932
930
  SCHEMA = `
933
931
  PRAGMA journal_mode = WAL;
@@ -953,7 +951,8 @@ CREATE TABLE IF NOT EXISTS items (
953
951
  target TEXT,
954
952
  text TEXT,
955
953
  status TEXT NOT NULL,
956
- data TEXT NOT NULL
954
+ data TEXT NOT NULL,
955
+ author TEXT NOT NULL DEFAULT ''
957
956
  );
958
957
  CREATE INDEX IF NOT EXISTS items_artifact ON items(artifact_id, at);
959
958
  CREATE TABLE IF NOT EXISTS replies (
@@ -971,6 +970,9 @@ CREATE TABLE IF NOT EXISTS replies (
971
970
  constructor(db) {
972
971
  this.db = db;
973
972
  db.exec(SCHEMA);
973
+ if (!db.prepare("PRAGMA table_info(items)").all().some((c) => c.name === "author")) {
974
+ db.exec("ALTER TABLE items ADD COLUMN author TEXT NOT NULL DEFAULT ''");
975
+ }
974
976
  this.q = {
975
977
  upsertArtifact: db.prepare(`
976
978
  INSERT INTO artifacts (id, file, project, title, created_at, updated_at, reviewed, status, ended_by)
@@ -982,9 +984,9 @@ CREATE TABLE IF NOT EXISTS replies (
982
984
  status = COALESCE(excluded.status, artifacts.status),
983
985
  ended_by = CASE WHEN excluded.status IS NULL THEN artifacts.ended_by ELSE excluded.ended_by END`),
984
986
  upsertItem: db.prepare(`
985
- INSERT INTO items (id, artifact_id, entry_id, at, kind, target, text, status, data)
986
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
987
- ON CONFLICT(id) DO UPDATE SET status = excluded.status, text = excluded.text, data = excluded.data`),
987
+ INSERT INTO items (id, artifact_id, entry_id, at, kind, target, text, status, data, author)
988
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
989
+ ON CONFLICT(id) DO UPDATE SET status = excluded.status, text = excluded.text, data = excluded.data, author = excluded.author`),
988
990
  upsertReply: db.prepare(`INSERT INTO replies (id, artifact_id, at, text) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING`),
989
991
  artifact: db.prepare(`SELECT * FROM artifacts WHERE id = ?`)
990
992
  };
@@ -1011,7 +1013,7 @@ CREATE TABLE IF NOT EXISTS replies (
1011
1013
  const status = itemStatus(e, pending.has(e.id));
1012
1014
  (e.items || []).forEach((it2, i) => {
1013
1015
  const id = it2.id || `${e.id}.${i}`;
1014
- this.q.upsertItem.run(id, s.id, e.id, e.at, it2.kind || "message", it2.target || it2.statement || null, itemText(it2), status, JSON.stringify(it2));
1016
+ this.q.upsertItem.run(id, s.id, e.id, e.at, it2.kind || "message", it2.target || it2.statement || null, itemText(it2), status, JSON.stringify(it2), e.author || "");
1015
1017
  });
1016
1018
  }
1017
1019
  this.db.exec("COMMIT");
@@ -1030,29 +1032,154 @@ CREATE TABLE IF NOT EXISTS replies (
1030
1032
  `SELECT a.*,
1031
1033
  SUM(CASE WHEN i.status = 'open' THEN 1 ELSE 0 END) AS open,
1032
1034
  SUM(CASE WHEN i.status = 'received' THEN 1 ELSE 0 END) AS received,
1033
- SUM(CASE WHEN i.status = 'addressed' THEN 1 ELSE 0 END) AS addressed
1035
+ SUM(CASE WHEN i.status = 'addressed' THEN 1 ELSE 0 END) AS addressed,
1036
+ SUM(CASE WHEN i.status = 'held' THEN 1 ELSE 0 END) AS held
1034
1037
  FROM artifacts a LEFT JOIN items i ON i.artifact_id = a.id
1035
1038
  WHERE (? = '' OR a.title LIKE ? ESCAPE '\\' OR a.file LIKE ? ESCAPE '\\' OR a.project LIKE ? ESCAPE '\\')
1036
1039
  AND (? = '' OR a.project = ?)
1037
1040
  GROUP BY a.id ORDER BY a.updated_at DESC LIMIT ?`
1038
- ).all(q2, like, like, like, project, project, limit).map((r) => ({ ...r, open: r.open || 0, received: r.received || 0, addressed: r.addressed || 0 }));
1041
+ ).all(q2, like, like, like, project, project, limit).map((r) => ({ ...r, open: r.open || 0, received: r.received || 0, addressed: r.addressed || 0, held: r.held || 0 }));
1042
+ }
1043
+ /**
1044
+ * Notes (annotations and messages) across all pages, newest first.
1045
+ * `q` matches note text, target and page title. `status`: "unanswered" (held, open and received), "answered", or "" for all.
1046
+ * `author`: a reviewer's name, "owner" for the owner's own notes, or "" for everyone. Discarded notes are left out.
1047
+ * @param {{ q?: string, status?: string, project?: string, author?: string, limit?: number }} [o]
1048
+ */
1049
+ notes({ q: q2 = "", status = "", project = "", author = "", limit = 200 } = {}) {
1050
+ const anyAuthor = author === "" ? "all" : "one";
1051
+ const who = author === "owner" ? "" : author;
1052
+ const like = `%${q2.replace(/[%_\\]/g, (c) => `\\${c}`)}%`;
1053
+ return this.db.prepare(
1054
+ `SELECT i.id, i.artifact_id, i.at, i.kind, i.target, i.text, i.status, i.author,
1055
+ a.title, a.file, a.project
1056
+ FROM items i JOIN artifacts a ON a.id = i.artifact_id
1057
+ WHERE i.kind IN ('annotation', 'message') AND i.status != 'discarded'
1058
+ AND (? = 'all' OR i.author = ?)
1059
+ AND (? = '' OR i.text LIKE ? ESCAPE '\\' OR i.target LIKE ? ESCAPE '\\' OR a.title LIKE ? ESCAPE '\\')
1060
+ AND (? = '' OR a.project = ?)
1061
+ AND (? = '' OR (? = 'unanswered' AND i.status != 'addressed') OR (? = 'answered' AND i.status = 'addressed'))
1062
+ ORDER BY i.at DESC, i.id LIMIT ?`
1063
+ ).all(anyAuthor, who, q2, like, like, like, project, project, status, status, status, limit);
1064
+ }
1065
+ /** Reviewer names with notes in the index (the owner's own notes are not listed). */
1066
+ authors() {
1067
+ return this.db.prepare(`SELECT DISTINCT author FROM items WHERE author != '' AND status != 'discarded' ORDER BY author COLLATE NOCASE`).all().map((r) => String(r.author));
1039
1068
  }
1040
1069
  stats() {
1041
1070
  return this.db.prepare(
1042
1071
  `SELECT (SELECT COUNT(*) FROM artifacts) AS artifacts, (SELECT COUNT(DISTINCT project) FROM artifacts) AS projects,
1043
- (SELECT COUNT(*) FROM items WHERE status != 'addressed') AS unanswered`
1072
+ (SELECT COUNT(*) FROM items WHERE status IN ('open', 'received', 'held')) AS unanswered`
1044
1073
  ).get();
1045
1074
  }
1046
1075
  };
1047
1076
  }
1048
1077
  });
1049
1078
 
1050
- // src/cli/sessions.js
1079
+ // src/cli/invites.js
1051
1080
  import crypto from "node:crypto";
1052
1081
  import fs6 from "node:fs";
1053
1082
  import path5 from "node:path";
1083
+ function safeEqual(a, b) {
1084
+ const x = crypto.createHash("sha256").update(String(a)).digest();
1085
+ const y = crypto.createHash("sha256").update(String(b)).digest();
1086
+ return crypto.timingSafeEqual(x, y);
1087
+ }
1088
+ function cleanName(raw) {
1089
+ const name = String(raw ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
1090
+ if (!name) throw new Error('a reviewer name is required: --as "<name>"');
1091
+ if (name.length > 40) throw new Error("the reviewer name is longer than 40 characters");
1092
+ if (RESERVED.has(name.toLowerCase())) throw new Error(`"${name}" is reserved; pick another name`);
1093
+ return name;
1094
+ }
1095
+ function tokenFromCookie(header, name = COOKIE) {
1096
+ for (const part of String(header || "").split(";")) {
1097
+ const i = part.indexOf("=");
1098
+ if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
1099
+ }
1100
+ return null;
1101
+ }
1102
+ function inviteId(inv) {
1103
+ return inv.token.slice(0, 8);
1104
+ }
1105
+ function ownerKey(dir) {
1106
+ const file2 = path5.join(dir, "owner.key");
1107
+ try {
1108
+ const key2 = fs6.readFileSync(file2, "utf8").trim();
1109
+ if (key2.length >= 22) return key2;
1110
+ } catch {
1111
+ }
1112
+ const key = crypto.randomBytes(24).toString("base64url");
1113
+ fs6.mkdirSync(dir, { recursive: true });
1114
+ fs6.writeFileSync(file2, key + "\n", { mode: 384 });
1115
+ return key;
1116
+ }
1117
+ var COOKIE, RESERVED, InviteStore, OWNER_COOKIE;
1118
+ var init_invites = __esm({
1119
+ "src/cli/invites.js"() {
1120
+ COOKIE = "austere_rv";
1121
+ RESERVED = /* @__PURE__ */ new Set(["owner", "you", "agent", "me"]);
1122
+ InviteStore = class {
1123
+ /** @param {string} dir state dir */
1124
+ constructor(dir) {
1125
+ this.file = path5.join(dir, "invites.json");
1126
+ this.invites = [];
1127
+ try {
1128
+ const raw = JSON.parse(fs6.readFileSync(this.file, "utf8"));
1129
+ if (Array.isArray(raw.invites)) {
1130
+ this.invites = raw.invites.filter((i) => i && typeof i.token === "string" && i.token.length >= 22 && i.name && i.sessionId);
1131
+ }
1132
+ } catch {
1133
+ }
1134
+ }
1135
+ save() {
1136
+ const tmp = `${this.file}.${process.pid}.tmp`;
1137
+ fs6.writeFileSync(tmp, JSON.stringify({ invites: this.invites }, null, 2) + "\n", { mode: 384 });
1138
+ fs6.renameSync(tmp, this.file);
1139
+ }
1140
+ /** @returns {Invite} */
1141
+ add({ name, sessionId: sessionId2, file: file2, auto = false }) {
1142
+ const clean = cleanName(name);
1143
+ if (this.invites.some((i) => i.sessionId === sessionId2 && i.name.toLowerCase() === clean.toLowerCase())) {
1144
+ throw new Error(`this page is already shared with "${clean}"; revoke that invite first (share revoke "${clean}")`);
1145
+ }
1146
+ const inv = { token: crypto.randomBytes(24).toString("base64url"), name: clean, sessionId: sessionId2, file: file2, auto: Boolean(auto), createdAt: Date.now() };
1147
+ this.invites.push(inv);
1148
+ this.save();
1149
+ return inv;
1150
+ }
1151
+ /** The invite for a token, or null. Compares every token, so timing does not show a near miss. */
1152
+ find(token) {
1153
+ if (typeof token !== "string" || !token) return null;
1154
+ let hit = null;
1155
+ for (const inv of this.invites) if (safeEqual(inv.token, token) && !hit) hit = inv;
1156
+ return hit;
1157
+ }
1158
+ list() {
1159
+ return [...this.invites];
1160
+ }
1161
+ /** Remove the invites a key names: a full token, its short id, or a reviewer name. Returns what was removed. */
1162
+ revoke(key) {
1163
+ const k = String(key || "").trim();
1164
+ if (!k) return [];
1165
+ const gone = this.invites.filter((i) => safeEqual(i.token, k) || k.length === 8 && inviteId(i) === k || i.name.toLowerCase() === k.toLowerCase());
1166
+ if (gone.length) {
1167
+ this.invites = this.invites.filter((i) => !gone.includes(i));
1168
+ this.save();
1169
+ }
1170
+ return gone;
1171
+ }
1172
+ };
1173
+ OWNER_COOKIE = "austere_owner";
1174
+ }
1175
+ });
1176
+
1177
+ // src/cli/sessions.js
1178
+ import crypto2 from "node:crypto";
1179
+ import fs7 from "node:fs";
1180
+ import path6 from "node:path";
1054
1181
  function sessionId(file2) {
1055
- return crypto.createHash("sha1").update(file2).digest("hex").slice(0, 12);
1182
+ return crypto2.createHash("sha1").update(file2).digest("hex").slice(0, 12);
1056
1183
  }
1057
1184
  function newId(prefix) {
1058
1185
  return `${prefix}${Date.now().toString(36)}${(++seq).toString(36)}`;
@@ -1061,7 +1188,7 @@ function hydrate(s) {
1061
1188
  return {
1062
1189
  id: s.id,
1063
1190
  file: s.file,
1064
- title: s.title || path5.basename(s.file),
1191
+ title: s.title || path6.basename(s.file),
1065
1192
  createdAt: s.createdAt || Date.now(),
1066
1193
  updatedAt: s.updatedAt || Date.now(),
1067
1194
  status: s.status || "open",
@@ -1082,17 +1209,20 @@ var init_sessions = __esm({
1082
1209
  MAX_TRANSCRIPT = 400;
1083
1210
  seq = 0;
1084
1211
  SessionStore = class {
1085
- /** @param {{ onFlush?: (session) => void }} [options] onFlush runs after a session is written (the hub mirrors it) */
1212
+ /**
1213
+ * @param {string} dir
1214
+ * @param {{ onFlush?: (session) => void }} [options] onFlush runs after a session is written (the hub mirrors it)
1215
+ */
1086
1216
  constructor(dir, { onFlush } = {}) {
1087
1217
  this.onFlush = onFlush || null;
1088
- this.dir = path5.join(dir, "sessions");
1089
- fs6.mkdirSync(this.dir, { recursive: true });
1218
+ this.dir = path6.join(dir, "sessions");
1219
+ fs7.mkdirSync(this.dir, { recursive: true });
1090
1220
  this.sessions = /* @__PURE__ */ new Map();
1091
1221
  this.timers = /* @__PURE__ */ new Map();
1092
- for (const f of fs6.readdirSync(this.dir)) {
1222
+ for (const f of fs7.readdirSync(this.dir)) {
1093
1223
  if (!f.endsWith(".json")) continue;
1094
1224
  try {
1095
- const s = JSON.parse(fs6.readFileSync(path5.join(this.dir, f), "utf8"));
1225
+ const s = JSON.parse(fs7.readFileSync(path6.join(this.dir, f), "utf8"));
1096
1226
  if (s && s.id && s.file) this.sessions.set(s.id, hydrate(s));
1097
1227
  } catch {
1098
1228
  }
@@ -1139,11 +1269,11 @@ var init_sessions = __esm({
1139
1269
  pending: s.pending,
1140
1270
  transcript: s.transcript.slice(-MAX_TRANSCRIPT)
1141
1271
  };
1142
- const file2 = path5.join(this.dir, `${s.id}.json`);
1272
+ const file2 = path6.join(this.dir, `${s.id}.json`);
1143
1273
  const tmp = `${file2}.${process.pid}.tmp`;
1144
1274
  try {
1145
- fs6.writeFileSync(tmp, JSON.stringify(data));
1146
- fs6.renameSync(tmp, file2);
1275
+ fs7.writeFileSync(tmp, JSON.stringify(data));
1276
+ fs7.renameSync(tmp, file2);
1147
1277
  } catch {
1148
1278
  }
1149
1279
  if (this.onFlush) {
@@ -1157,14 +1287,46 @@ var init_sessions = __esm({
1157
1287
  for (const s of this.sessions.values()) if (this.timers.has(s.id)) this.flush(s);
1158
1288
  }
1159
1289
  // ── conversation ──
1160
- /** Reviewer sent a batch of items. Returns the transcript entry. */
1161
- addUserBatch(s, items, { end = false } = {}) {
1290
+ /**
1291
+ * Reviewer sent a batch of items. Returns the transcript entry.
1292
+ * `author` is a team reviewer's name (empty: the owner). `hold` keeps the entry out of
1293
+ * `pending` until the owner releases it.
1294
+ */
1295
+ addUserBatch(s, items, { end = false, author = "", hold = false } = {}) {
1162
1296
  const entry = { id: newId("u"), role: "user", at: Date.now(), items, end: end || void 0, seenAt: null, doneAt: null };
1297
+ if (author) entry.author = author;
1298
+ if (hold) entry.held = true;
1163
1299
  s.transcript.push(entry);
1164
- s.pending.push(entry.id);
1300
+ if (!hold) s.pending.push(entry.id);
1165
1301
  this.touch(s);
1166
1302
  return entry;
1167
1303
  }
1304
+ /** Held entries, oldest first. */
1305
+ heldEntries(s) {
1306
+ return s.transcript.filter((e) => e.role === "user" && e.held);
1307
+ }
1308
+ /** The owner sends held notes on to the agent (`ids` omitted: all of them). Returns the entries released. */
1309
+ release(s, ids) {
1310
+ const want = ids ? new Set(ids) : null;
1311
+ const out = this.heldEntries(s).filter((e) => !want || want.has(e.id));
1312
+ for (const e of out) {
1313
+ e.held = false;
1314
+ s.pending.push(e.id);
1315
+ }
1316
+ if (out.length) this.touch(s);
1317
+ return out;
1318
+ }
1319
+ /** The owner drops held notes. They stay on record, marked discarded, and never reach the agent. */
1320
+ discard(s, ids) {
1321
+ const want = ids ? new Set(ids) : null;
1322
+ const out = this.heldEntries(s).filter((e) => !want || want.has(e.id));
1323
+ for (const e of out) {
1324
+ e.held = false;
1325
+ e.discarded = true;
1326
+ }
1327
+ if (out.length) this.touch(s);
1328
+ return out;
1329
+ }
1168
1330
  addAgentReply(s, text) {
1169
1331
  const now = Date.now();
1170
1332
  for (const e of s.transcript) if (e.role === "user" && e.seenAt && !e.doneAt) e.doneAt = now;
@@ -1182,10 +1344,13 @@ var init_sessions = __esm({
1182
1344
  */
1183
1345
  feedback(s, { all = false, peek = false } = {}) {
1184
1346
  const pending = new Set(s.pending);
1185
- const picked = s.transcript.filter((e) => e.role === "user" && (e.items || []).length && (all || pending.has(e.id) || !e.doneAt));
1347
+ const picked = s.transcript.filter(
1348
+ (e) => e.role === "user" && !e.held && !e.discarded && (e.items || []).length && (all || pending.has(e.id) || !e.doneAt)
1349
+ );
1186
1350
  const entries = picked.map((e) => ({
1187
1351
  id: e.id,
1188
1352
  at: e.at,
1353
+ author: e.author || void 0,
1189
1354
  items: e.items,
1190
1355
  status: pending.has(e.id) ? "open" : e.doneAt ? "addressed" : "received"
1191
1356
  }));
@@ -1223,8 +1388,8 @@ var init_sessions = __esm({
1223
1388
  });
1224
1389
 
1225
1390
  // src/cli/export.js
1226
- import fs7 from "node:fs";
1227
- import path6 from "node:path";
1391
+ import fs8 from "node:fs";
1392
+ import path7 from "node:path";
1228
1393
  function inlineScript(code) {
1229
1394
  return `<script>${code.replace(/<\/script/gi, "<\\/script")}</script>`;
1230
1395
  }
@@ -1234,12 +1399,12 @@ function exportHtml(html) {
1234
1399
  if (art.program == null) throw new Error('no <script type="text/openui"> block');
1235
1400
  let tags = "";
1236
1401
  if (/\bMermaid\s*\(/.test(art.program)) {
1237
- tags += inlineScript(fs7.readFileSync(assetPath("mermaid.min.js"), "utf8")) + "\n";
1402
+ tags += inlineScript(fs8.readFileSync(assetPath("mermaid.min.js"), "utf8")) + "\n";
1238
1403
  }
1239
- tags += inlineScript(fs7.readFileSync(assetPath("austere.js"), "utf8"));
1240
- for (const p of implicitPacks(art.program)) tags += "\n" + inlineScript(fs7.readFileSync(path6.join(p.dir, "pack.js"), "utf8"));
1404
+ tags += inlineScript(fs8.readFileSync(assetPath("austere.js"), "utf8"));
1405
+ for (const p of implicitPacks(art.program)) tags += "\n" + inlineScript(fs8.readFileSync(path7.join(p.dir, "pack.js"), "utf8"));
1241
1406
  const packs = pagePacks(html);
1242
- for (const p of packs.installed) tags += "\n" + inlineScript(fs7.readFileSync(path6.join(p.dir, "pack.js"), "utf8"));
1407
+ for (const p of packs.installed) tags += "\n" + inlineScript(fs8.readFileSync(path7.join(p.dir, "pack.js"), "utf8"));
1243
1408
  for (const p of [...packs.missing, ...packs.mismatched]) {
1244
1409
  notices.push(`Pack ${p.name}@${p.version} is not installed here, so the exported page loads it from the CDN (needs network).`);
1245
1410
  }
@@ -1267,10 +1432,10 @@ __export(install_exports, {
1267
1432
  removePack: () => removePack,
1268
1433
  untar: () => untar
1269
1434
  });
1270
- import fs8 from "node:fs";
1271
- import path7 from "node:path";
1435
+ import fs9 from "node:fs";
1436
+ import path8 from "node:path";
1272
1437
  import zlib from "node:zlib";
1273
- import crypto2 from "node:crypto";
1438
+ import crypto3 from "node:crypto";
1274
1439
  function registryUrl() {
1275
1440
  const r = process.env.AUSTERE_AXI_NPM_REGISTRY || process.env.npm_config_registry || "https://registry.npmjs.org/";
1276
1441
  return r.endsWith("/") ? r : r + "/";
@@ -1279,7 +1444,7 @@ function looksLikePath(spec) {
1279
1444
  return /^(\.{1,2}([\\/]|$)|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(spec) || /\.t(ar\.)?gz$/i.test(spec) || spec.includes("\\") || spec.includes("/") && !spec.startsWith("@");
1280
1445
  }
1281
1446
  function sri(alg, buf) {
1282
- return `${alg}-${crypto2.createHash(alg).update(buf).digest("base64")}`;
1447
+ return `${alg}-${crypto3.createHash(alg).update(buf).digest("base64")}`;
1283
1448
  }
1284
1449
  function untar(buf) {
1285
1450
  const files = /* @__PURE__ */ new Map();
@@ -1350,13 +1515,11 @@ async function fetchBuffer(url2, accept) {
1350
1515
  try {
1351
1516
  res = await fetch(url2, { headers: accept ? { accept } : {}, redirect: "follow" });
1352
1517
  } catch (err) {
1353
- const e = new Error(`could not reach ${url2} (${err.cause?.code || err.message})`);
1354
- e.network = true;
1518
+ const e = Object.assign(new Error(`could not reach ${url2} (${err.cause?.code || err.message})`), { network: true });
1355
1519
  throw e;
1356
1520
  }
1357
1521
  if (!res.ok) {
1358
- const e = new Error(`${url2}: HTTP ${res.status}`);
1359
- e.status = res.status;
1522
+ const e = Object.assign(new Error(`${url2}: HTTP ${res.status}`), { status: res.status });
1360
1523
  throw e;
1361
1524
  }
1362
1525
  const len = Number(res.headers.get("content-length") || 0);
@@ -1386,7 +1549,7 @@ async function fromRegistry(spec) {
1386
1549
  if (!["sha512", "sha384", "sha256"].includes(alg) || sri(alg, tgz) !== dist.integrity) {
1387
1550
  throw new Error(`${name}@${version2}: the tarball does not match the registry's integrity hash`);
1388
1551
  }
1389
- } else if (dist.shasum && crypto2.createHash("sha1").update(tgz).digest("hex") !== dist.shasum) {
1552
+ } else if (dist.shasum && crypto3.createHash("sha1").update(tgz).digest("hex") !== dist.shasum) {
1390
1553
  throw new Error(`${name}@${version2}: the tarball does not match the registry's shasum`);
1391
1554
  }
1392
1555
  return { ...readTarball(tgz, `${name}@${version2}`), expect: { name, version: version2 }, source: "npm", resolved: dist.tarball };
@@ -1406,15 +1569,15 @@ function readTarball(tgz, label) {
1406
1569
  return { packJs, catalog, packageJson: files.get("package.json") || null };
1407
1570
  }
1408
1571
  function fromTarball(file2) {
1409
- const abs = path7.resolve(file2.replace(/^~(?=[\\/])/, process.env.HOME || ""));
1410
- if (!fs8.existsSync(abs)) throw new Error(`no such file: ${abs}`);
1411
- return { ...readTarball(fs8.readFileSync(abs), path7.basename(abs)), source: "tarball", resolved: abs };
1572
+ const abs = path8.resolve(file2.replace(/^~(?=[\\/])/, process.env.HOME || ""));
1573
+ if (!fs9.existsSync(abs)) throw new Error(`no such file: ${abs}`);
1574
+ return { ...readTarball(fs9.readFileSync(abs), path8.basename(abs)), source: "tarball", resolved: abs };
1412
1575
  }
1413
1576
  function fromFolder(dir) {
1414
- const abs = path7.resolve(dir.replace(/^~(?=[\\/])/, process.env.HOME || ""));
1415
- if (!fs8.existsSync(abs)) throw new Error(`no such folder: ${abs}`);
1416
- if (fs8.statSync(abs).isFile()) return fromTarball(abs);
1417
- const read = (rel) => fs8.existsSync(path7.join(abs, rel)) ? fs8.readFileSync(path7.join(abs, rel)) : null;
1577
+ const abs = path8.resolve(dir.replace(/^~(?=[\\/])/, process.env.HOME || ""));
1578
+ if (!fs9.existsSync(abs)) throw new Error(`no such folder: ${abs}`);
1579
+ if (fs9.statSync(abs).isFile()) return fromTarball(abs);
1580
+ const read = (rel) => fs9.existsSync(path8.join(abs, rel)) ? fs9.readFileSync(path8.join(abs, rel)) : null;
1418
1581
  const packJs = read("dist/pack.js");
1419
1582
  const catalog = read("dist/catalog.json");
1420
1583
  if (!packJs || !catalog) throw new Error(`${abs} has no dist/pack.js and dist/catalog.json: run \`austere-axi components build ${dir}\` first`);
@@ -1444,21 +1607,23 @@ async function addPack(spec, { trust = false } = {}) {
1444
1607
  if (!local) {
1445
1608
  const { name } = spec.startsWith("@") || spec.includes("@") ? splitSpec(spec) : { name: spec };
1446
1609
  if (!isFirstParty(name) && !trust) {
1447
- const e = new Error(
1448
- `${name} is a third-party pack: its code would run inside your review pages, where it can read the page and add items to the feedback queue. Look at its source first (npm view ${name} repository), then rerun with --trust.`
1610
+ throw Object.assign(
1611
+ new Error(
1612
+ `${name} is a third-party pack: its code would run inside your review pages, where it can read the page and add items to the feedback queue. Look at its source first (npm view ${name} repository), then rerun with --trust.`
1613
+ ),
1614
+ { code: "untrusted" }
1449
1615
  );
1450
- e.code = "untrusted";
1451
- throw e;
1452
1616
  }
1453
1617
  }
1454
1618
  const got = local ? fromFolder(spec) : await fromRegistry(spec);
1455
1619
  if (got.source === "tarball" && !trust) {
1456
- const peek = parseCatalog(got.catalog, path7.basename(got.resolved));
1457
- const e = new Error(
1458
- `${path7.basename(got.resolved)} (${peek.pack}) comes from a file, not the registry: its code would run inside your review pages. Rerun with --trust once you have looked at it.`
1620
+ const peek = parseCatalog(got.catalog, path8.basename(got.resolved));
1621
+ throw Object.assign(
1622
+ new Error(
1623
+ `${path8.basename(got.resolved)} (${peek.pack}) comes from a file, not the registry: its code would run inside your review pages. Rerun with --trust once you have looked at it.`
1624
+ ),
1625
+ { code: "untrusted" }
1459
1626
  );
1460
- e.code = "untrusted";
1461
- throw e;
1462
1627
  }
1463
1628
  const label = got.expect ? `${got.expect.name}@${got.expect.version}` : spec;
1464
1629
  const cat = parseCatalog(got.catalog, label);
@@ -1480,14 +1645,14 @@ async function addPack(spec, { trust = false } = {}) {
1480
1645
  }
1481
1646
  const lock = readLock();
1482
1647
  const previous = lock.packs[cat.pack];
1483
- const dir = path7.join(packsDir(), packDirName(cat.pack, cat.version));
1484
- fs8.rmSync(dir, { recursive: true, force: true });
1485
- fs8.mkdirSync(dir, { recursive: true });
1486
- fs8.writeFileSync(path7.join(dir, "pack.js"), got.packJs);
1487
- fs8.writeFileSync(path7.join(dir, "catalog.json"), got.catalog);
1488
- if (got.packageJson) fs8.writeFileSync(path7.join(dir, "package.json"), got.packageJson);
1648
+ const dir = path8.join(packsDir(), packDirName(cat.pack, cat.version));
1649
+ fs9.rmSync(dir, { recursive: true, force: true });
1650
+ fs9.mkdirSync(dir, { recursive: true });
1651
+ fs9.writeFileSync(path8.join(dir, "pack.js"), got.packJs);
1652
+ fs9.writeFileSync(path8.join(dir, "catalog.json"), got.catalog);
1653
+ if (got.packageJson) fs9.writeFileSync(path8.join(dir, "package.json"), got.packageJson);
1489
1654
  if (previous && previous.version !== cat.version) {
1490
- fs8.rmSync(path7.join(packsDir(), packDirName(cat.pack, previous.version)), { recursive: true, force: true });
1655
+ fs9.rmSync(path8.join(packsDir(), packDirName(cat.pack, previous.version)), { recursive: true, force: true });
1491
1656
  }
1492
1657
  lock.packs[cat.pack] = {
1493
1658
  version: cat.version,
@@ -1507,7 +1672,7 @@ function removePack(name) {
1507
1672
  if (!entry) return null;
1508
1673
  delete lock.packs[name];
1509
1674
  writeLock(lock);
1510
- fs8.rmSync(path7.join(packsDir(), packDirName(name, entry.version)), { recursive: true, force: true });
1675
+ fs9.rmSync(path8.join(packsDir(), packDirName(name, entry.version)), { recursive: true, force: true });
1511
1676
  return { name, version: entry.version };
1512
1677
  }
1513
1678
  var MAX_TARBALL, MAX_PACK_JS;
@@ -1527,8 +1692,8 @@ __export(pack_dev_exports, {
1527
1692
  buildPackDir: () => buildPackDir,
1528
1693
  newPack: () => newPack
1529
1694
  });
1530
- import fs9 from "node:fs";
1531
- import path8 from "node:path";
1695
+ import fs10 from "node:fs";
1696
+ import path9 from "node:path";
1532
1697
  import { createRequire } from "node:module";
1533
1698
  import { pathToFileURL } from "node:url";
1534
1699
  function packageJson(name) {
@@ -1553,7 +1718,7 @@ Components for [austere-axi](https://github.com/MandalorianBatman/austere-axi) r
1553
1718
  npm install
1554
1719
  npm run build # dist/pack.js + dist/catalog.json
1555
1720
  npx austere-axi components add . # use it on this machine
1556
- npx austere-axi components show Timeline
1721
+ npx austere-axi components show Milestones
1557
1722
  \`\`\`
1558
1723
 
1559
1724
  A page that uses it must declare it: \`austere-axi check --fix page.html\` writes the
@@ -1564,10 +1729,10 @@ Publish with \`npm publish\` after \`npm run build\`. Others install it with
1564
1729
  `;
1565
1730
  }
1566
1731
  function newPack(dir, { name } = {}) {
1567
- if (!dir) throw new Error("new needs a folder: austere-axi components new austere-pack-timeline");
1568
- const abs = path8.resolve(dir);
1569
- if (fs9.existsSync(abs) && fs9.readdirSync(abs).length) throw new Error(`${abs} is not empty`);
1570
- const pkgName = name || path8.basename(abs).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
1732
+ if (!dir) throw new Error("new needs a folder: austere-axi components new austere-pack-milestones");
1733
+ const abs = path9.resolve(dir);
1734
+ if (fs10.existsSync(abs) && fs10.readdirSync(abs).length) throw new Error(`${abs} is not empty`);
1735
+ const pkgName = name || path9.basename(abs).toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
1571
1736
  if (!isValidPackName(pkgName)) throw new Error(`${JSON.stringify(pkgName)} is not an npm package name; pass --name`);
1572
1737
  const files = {
1573
1738
  "package.json": JSON.stringify(packageJson(pkgName), null, 2) + "\n",
@@ -1576,13 +1741,13 @@ function newPack(dir, { name } = {}) {
1576
1741
  ...TEMPLATE
1577
1742
  };
1578
1743
  for (const [rel, text] of Object.entries(files)) {
1579
- fs9.mkdirSync(path8.dirname(path8.join(abs, rel)), { recursive: true });
1580
- fs9.writeFileSync(path8.join(abs, rel), text);
1744
+ fs10.mkdirSync(path9.dirname(path9.join(abs, rel)), { recursive: true });
1745
+ fs10.writeFileSync(path9.join(abs, rel), text);
1581
1746
  }
1582
1747
  return { dir: abs, name: pkgName, files: Object.keys(files) };
1583
1748
  }
1584
1749
  async function findEsbuild(dir) {
1585
- for (const base of [path8.join(dir, "package.json"), path8.join(process.cwd(), "package.json"), import.meta.url]) {
1750
+ for (const base of [path9.join(dir, "package.json"), path9.join(process.cwd(), "package.json"), import.meta.url]) {
1586
1751
  try {
1587
1752
  const resolved = createRequire(base).resolve("esbuild");
1588
1753
  return await import(pathToFileURL(resolved).href);
@@ -1592,8 +1757,8 @@ async function findEsbuild(dir) {
1592
1757
  return null;
1593
1758
  }
1594
1759
  async function buildPackDir(dir = ".") {
1595
- const abs = path8.resolve(dir);
1596
- if (!fs9.existsSync(path8.join(abs, "package.json"))) throw new Error(`${abs} has no package.json (\`austere-axi components new <dir>\` makes a pack)`);
1760
+ const abs = path9.resolve(dir);
1761
+ if (!fs10.existsSync(path9.join(abs, "package.json"))) throw new Error(`${abs} has no package.json (\`austere-axi components new <dir>\` makes a pack)`);
1597
1762
  const esbuild = await findEsbuild(abs);
1598
1763
  if (!esbuild) throw new Error(`components build needs esbuild: run \`npm install -D esbuild\` in ${abs}`);
1599
1764
  process.env.OPENUI_TELEMETRY_DISABLED ||= "1";
@@ -1620,12 +1785,12 @@ var init_pack_dev = __esm({
1620
1785
  init_packs_meta();
1621
1786
  TEMPLATE = {
1622
1787
  "src/index.js": `// The pack: every component it adds, plus CSS the runtime puts on the page once.
1623
- import Timeline from "./Timeline.jsx";
1788
+ import Milestones from "./Milestones.jsx";
1624
1789
  import css from "./style.css";
1625
1790
 
1626
- export default { components: [Timeline], css };
1791
+ export default { components: [Milestones], css };
1627
1792
  `,
1628
- "src/Timeline.jsx": `// One component per file. austereComponent is the same contract the core
1793
+ "src/Milestones.jsx": `// One component per file. austereComponent is the same contract the core
1629
1794
  // components follow: PascalCase name, zod props in positional order (append
1630
1795
  // only once published), a one-line description, an example program.
1631
1796
  // React, zod and "austere-axi/pack" come from the page's runtime; do not
@@ -1646,10 +1811,10 @@ function parseEvents(lines) {
1646
1811
  });
1647
1812
  }
1648
1813
 
1649
- function TimelineView({ props }) {
1814
+ function MilestonesView({ props }) {
1650
1815
  const events = parseEvents(props.events);
1651
1816
  return (
1652
- <ol className="pack-timeline">
1817
+ <ol className="pack-milestones">
1653
1818
  {events.map((e, i) => (
1654
1819
  // data-austere-part makes each event its own annotation target.
1655
1820
  <li key={i} data-austere-part={e.what || e.when}>
@@ -1663,20 +1828,20 @@ function TimelineView({ props }) {
1663
1828
  }
1664
1829
 
1665
1830
  export default austereComponent({
1666
- name: "Timeline",
1831
+ name: "Milestones",
1667
1832
  // A core group (Layout Content Data Charts Forms Buttons) or a category of your own.
1668
1833
  group: "Content",
1669
1834
  props: z.object({
1670
1835
  events: z.array(z.string()),
1671
1836
  }),
1672
- description: 'Vertical timeline. events are "when | what | detail" strings (detail optional), oldest first. A click reaches the agent as the event.',
1673
- renderer: TimelineView,
1674
- example: \`launch = Timeline(["Mon | Freeze | branch cut", "Wed | Canary | 5% of traffic", "Fri | GA"])\`,
1837
+ description: 'Vertical milestones. events are "when | what | detail" strings (detail optional), oldest first. A click reaches the agent as the event.',
1838
+ renderer: MilestonesView,
1839
+ example: \`launch = Milestones(["Mon | Freeze | branch cut", "Wed | Canary | 5% of traffic", "Fri | GA"])\`,
1675
1840
  });
1676
1841
  `,
1677
1842
  "src/style.css": `/* Use the page's theme variables (--austere-*, --openui-*) so the pack fits the
1678
1843
  dark theme, and prefix every class with the pack's name. */
1679
- .pack-timeline {
1844
+ .pack-milestones {
1680
1845
  list-style: none;
1681
1846
  margin: 0;
1682
1847
  padding: 0 0 0 18px;
@@ -1686,13 +1851,13 @@ export default austereComponent({
1686
1851
  font-size: 14px;
1687
1852
  min-width: 0;
1688
1853
  }
1689
- .pack-timeline li {
1854
+ .pack-milestones li {
1690
1855
  position: relative;
1691
1856
  display: grid;
1692
1857
  gap: 2px;
1693
1858
  overflow-wrap: anywhere;
1694
1859
  }
1695
- .pack-timeline li::before {
1860
+ .pack-milestones li::before {
1696
1861
  content: "";
1697
1862
  position: absolute;
1698
1863
  left: -24px;
@@ -1702,11 +1867,11 @@ export default austereComponent({
1702
1867
  border-radius: 50%;
1703
1868
  background: var(--austere-accent);
1704
1869
  }
1705
- .pack-timeline time {
1870
+ .pack-milestones time {
1706
1871
  font-size: 12px;
1707
1872
  color: var(--austere-muted);
1708
1873
  }
1709
- .pack-timeline span {
1874
+ .pack-milestones span {
1710
1875
  color: var(--austere-muted);
1711
1876
  }
1712
1877
  `
@@ -1716,7 +1881,7 @@ export default austereComponent({
1716
1881
 
1717
1882
  // src/cli/net.js
1718
1883
  import { execFile } from "node:child_process";
1719
- import fs10 from "node:fs";
1884
+ import fs11 from "node:fs";
1720
1885
  function run(bin, args) {
1721
1886
  return new Promise((resolve) => {
1722
1887
  execFile(bin, args, { timeout: 2500, windowsHide: true }, (err, stdout) => resolve(err ? null : stdout));
@@ -1727,7 +1892,7 @@ async function detectTailscale({ fresh = false } = {}) {
1727
1892
  if (!fresh && Date.now() - cache.at < 3e4) return cache.value;
1728
1893
  let value = null;
1729
1894
  for (const bin of CANDIDATES) {
1730
- if (bin.startsWith("/") && !fs10.existsSync(bin)) continue;
1895
+ if (bin.startsWith("/") && !fs11.existsSync(bin)) continue;
1731
1896
  const out = await run(bin, ["status", "--json"]);
1732
1897
  if (!out) continue;
1733
1898
  try {
@@ -1775,7 +1940,7 @@ var init_net = __esm({
1775
1940
 
1776
1941
  // src/cli/hub-page.js
1777
1942
  import os2 from "node:os";
1778
- import path9 from "node:path";
1943
+ import path10 from "node:path";
1779
1944
  function day(ms) {
1780
1945
  const d = new Date(ms);
1781
1946
  const today = /* @__PURE__ */ new Date();
@@ -1790,7 +1955,7 @@ function time3(ms) {
1790
1955
  }
1791
1956
  function tilde(p) {
1792
1957
  const home = os2.homedir();
1793
- return p === home || p.startsWith(home + path9.sep) ? "~" + p.slice(home.length) : p;
1958
+ return p === home || p.startsWith(home + path10.sep) ? "~" + p.slice(home.length) : p;
1794
1959
  }
1795
1960
  function counts(a) {
1796
1961
  const waiting = a.open + a.received;
@@ -1824,13 +1989,13 @@ function hubPage(rows, { version: version2, q: q2, project, stats }) {
1824
1989
  const body = [...days.entries()].map(
1825
1990
  ([d, items]) => `<h3>${escapeHtml(d)}</h3><ul>${items.map(
1826
1991
  (a) => `<li><a href="/hub/open/${a.id}" title="${escapeHtml(a.file)}">
1827
- <strong>${escapeHtml(a.title || path9.basename(a.file))}</strong>
1828
- <span class="meta">${escapeHtml(path9.relative(proj, a.file) || path9.basename(a.file))} \xB7 ${time3(a.updated_at)} \xB7 ${state(a)}${counts(a) ? ` \xB7 ${counts(a)}` : ""}</span>
1992
+ <strong>${escapeHtml(a.title || path10.basename(a.file))}</strong>
1993
+ <span class="meta">${escapeHtml(path10.relative(proj, a.file) || path10.basename(a.file))} \xB7 ${time3(a.updated_at)} \xB7 ${state(a)}${counts(a) ? ` \xB7 ${counts(a)}` : ""}</span>
1829
1994
  </a></li>`
1830
1995
  ).join("")}</ul>`
1831
1996
  ).join("");
1832
1997
  const unanswered = list.reduce((n, a) => n + a.open + a.received, 0);
1833
- return `<section><h2><a href="/?project=${encodeURIComponent(proj)}">${escapeHtml(path9.basename(proj) || proj)}</a> <small title="${escapeHtml(proj)}">${escapeHtml(
1998
+ return `<section><h2><a href="/?project=${encodeURIComponent(proj)}">${escapeHtml(path10.basename(proj) || proj)}</a> <small title="${escapeHtml(proj)}">${escapeHtml(
1834
1999
  shortPath(proj)
1835
2000
  )}</small>${unanswered ? ` <b class="open">${unanswered} unanswered</b>` : ""}</h2>${body}</section>`;
1836
2001
  }).join("");
@@ -1839,7 +2004,42 @@ function hubPage(rows, { version: version2, q: q2, project, stats }) {
1839
2004
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
1840
2005
  <title>austere-axi hub</title><meta name="color-scheme" content="dark">
1841
2006
  <style>
1842
- body{margin:0;background:#0b0b0d;color:#e8e8ec;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
2007
+ ${STYLE}
2008
+ </style></head>
2009
+ <body><main>
2010
+ <header><h1>austere-axi hub<small>${escapeHtml(version2)}</small></h1><nav class="nav"><a href="/">Pages</a><a href="/notes">Notes</a></nav><span class="stats">${stats.artifacts} artifacts \xB7 ${stats.projects} projects${stats.unanswered ? ` \xB7 ${stats.unanswered} unanswered` : ""}</span></header>
2011
+ <form method="get" action="/">${project ? `<input type="hidden" name="project" value="${escapeHtml(project)}">` : ""}<input type="search" name="q" value="${escapeHtml(q2)}" placeholder="Search titles, files, projects" aria-label="Search"></form>
2012
+ ${filter}${sections || `<p class="empty">${empty}</p>`}
2013
+ </main></body></html>`;
2014
+ }
2015
+ function notesPage(rows, { version: version2, q: q2, project, status, author = "", authors = [], stats }) {
2016
+ const items = rows.map((n) => {
2017
+ const waiting = n.status !== "addressed";
2018
+ const when = `${day(n.at)} ${time3(n.at)}`;
2019
+ return `<li class="n"><a href="/hub/open/${escapeHtml(n.artifact_id)}" title="${escapeHtml(n.file)}">
2020
+ <p class="note">${escapeHtml(n.text || "(no text)")}</p>
2021
+ <span class="note-t">${n.author ? `<b class="by">${escapeHtml(n.author)}</b> \xB7 ` : ""}${n.target ? `on ${escapeHtml(n.target)} \xB7 ` : ""}<strong>${escapeHtml(n.title || path10.basename(n.file))}</strong> \xB7 ${escapeHtml(path10.basename(n.project) || n.project)} \xB7 ${escapeHtml(when)} \xB7 <span class="st ${waiting ? "waiting" : "done"}">${STATUS_LABEL[n.status] || escapeHtml(n.status)}</span></span>
2022
+ </a></li>`;
2023
+ }).join("");
2024
+ const opt = (v, label) => `<option value="${v}"${status === v ? " selected" : ""}>${label}</option>`;
2025
+ const people = authors.length ? `<select name="author" aria-label="Author"><option value="">Everyone</option><option value="owner"${author === "owner" ? " selected" : ""}>Owner</option>${authors.map((a) => `<option value="${escapeHtml(a)}"${author === a ? " selected" : ""}>${escapeHtml(a)}</option>`).join("")}</select>` : "";
2026
+ const filter = project ? `<p class="filter">Project ${escapeHtml(tilde(project))} \xB7 <a href="/notes">all projects</a></p>` : "";
2027
+ const empty = q2 || project || author || status !== "all" ? "No notes match." : "No notes yet.";
2028
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
2029
+ <title>Notes \xB7 austere-axi hub</title><meta name="color-scheme" content="dark">
2030
+ <style>
2031
+ ${STYLE}</style></head>
2032
+ <body><main>
2033
+ <header><h1>austere-axi hub<small>${escapeHtml(version2)}</small></h1><nav class="nav"><a href="/">Pages</a><a href="/notes">Notes</a></nav><span class="stats">${rows.length} notes${stats.unanswered ? ` \xB7 ${stats.unanswered} unanswered` : ""}</span></header>
2034
+ <form class="f${authors.length ? " p" : ""}" method="get" action="/notes">${project ? `<input type="hidden" name="project" value="${escapeHtml(project)}">` : ""}<input type="search" name="q" value="${escapeHtml(q2)}" placeholder="Search notes, targets, page titles" aria-label="Search notes"><select name="status" aria-label="Status">${opt("all", "All")}${opt("unanswered", "Unanswered")}${opt("answered", "Answered")}</select>${people}<button type="submit">Filter</button></form>
2035
+ ${filter}${items ? `<ul>${items}</ul>` : `<p class="empty">${empty}</p>`}
2036
+ </main></body></html>`;
2037
+ }
2038
+ var STYLE, STATUS_LABEL;
2039
+ var init_hub_page = __esm({
2040
+ "src/cli/hub-page.js"() {
2041
+ init_artifact();
2042
+ STYLE = `body{margin:0;background:#0b0b0d;color:#e8e8ec;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
1843
2043
  main{max-width:860px;margin:0 auto;padding:32px 16px 64px}
1844
2044
  header{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;justify-content:space-between}
1845
2045
  h1{font-size:18px;font-weight:600;margin:0}h1 small{color:#77777f;font-weight:400;font-size:13px;margin-left:6px}
@@ -1855,16 +2055,14 @@ li strong{color:#a9c1ff;font-weight:600;overflow-wrap:anywhere}li>a:hover strong
1855
2055
  .meta{color:#8a8a94;font-size:12px;overflow-wrap:anywhere}em{font-style:normal}em.live{color:#8fd19e}
1856
2056
  b{font-weight:600}b.open{color:#f2c46d;font-size:12px}
1857
2057
  .filter{color:#8a8a94;font-size:12.5px}.filter a{color:#a9c1ff}.empty{color:#8a8a94;margin-top:28px}
1858
- </style></head>
1859
- <body><main>
1860
- <header><h1>austere-axi hub<small>${escapeHtml(version2)}</small></h1><span class="stats">${stats.artifacts} artifacts \xB7 ${stats.projects} projects${stats.unanswered ? ` \xB7 ${stats.unanswered} unanswered` : ""}</span></header>
1861
- <form method="get" action="/">${project ? `<input type="hidden" name="project" value="${escapeHtml(project)}">` : ""}<input type="search" name="q" value="${escapeHtml(q2)}" placeholder="Search titles, files, projects" aria-label="Search"></form>
1862
- ${filter}${sections || `<p class="empty">${empty}</p>`}
1863
- </main></body></html>`;
1864
- }
1865
- var init_hub_page = __esm({
1866
- "src/cli/hub-page.js"() {
1867
- init_artifact();
2058
+ .nav{display:flex;gap:14px;font-size:13px}.nav a{color:#a9c1ff;text-decoration:none}
2059
+ form.f{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}button{background:#1c1c20;border:1px solid #2a2a30;border-radius:8px;color:#e8e8ec;padding:9px 12px;font:inherit}select{background:#141417;border:1px solid #2a2a30;border-radius:8px;color:#e8e8ec;padding:9px 12px;font:inherit;width:100%;box-sizing:border-box}
2060
+ .note{margin:0;overflow-wrap:anywhere;white-space:pre-wrap}.note-t{color:#8a8a94;font-size:12px}.st{font-size:12px}.st.waiting{color:#f2c46d}.st.done{color:#8fd19e}
2061
+ li.n{padding:12px 0}li.n>a{padding:0}
2062
+ b.by{color:#e8e8ec}
2063
+ @media(min-width:600px){form.f{grid-template-columns:minmax(0,1fr) 150px auto}form.f.p{grid-template-columns:minmax(0,1fr) 150px 150px auto}}
2064
+ `;
2065
+ STATUS_LABEL = { open: "unanswered", received: "unanswered", addressed: "answered", held: "held for the owner" };
1868
2066
  }
1869
2067
  });
1870
2068
 
@@ -1873,30 +2071,26 @@ var server_exports = {};
1873
2071
  __export(server_exports, {
1874
2072
  runServer: () => runServer
1875
2073
  });
1876
- import fs11 from "node:fs";
2074
+ import fs12 from "node:fs";
1877
2075
  import http2 from "node:http";
1878
- import path10 from "node:path";
1879
- import crypto3 from "node:crypto";
2076
+ import path11 from "node:path";
2077
+ import crypto4 from "node:crypto";
1880
2078
  async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
1881
2079
  const log = (...a) => {
1882
2080
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${a.join(" ")}`;
1883
2081
  if (verbose) process.stderr.write(line + "\n");
1884
2082
  try {
1885
- fs11.appendFileSync(path10.join(stateDir(), "server.log"), line + "\n");
2083
+ fs12.appendFileSync(path11.join(stateDir(), "server.log"), line + "\n");
1886
2084
  } catch {
1887
2085
  }
1888
2086
  };
1889
2087
  let hub = null;
1890
2088
  const hubWanted = hubEnabled();
1891
2089
  if (hubWanted) {
1892
- const why = hubUnsupported();
1893
- if (why) log("hub-unavailable", why);
1894
- else {
1895
- try {
1896
- hub = await Hub.open();
1897
- } catch (err) {
1898
- log("hub-unavailable", String(err?.message || err));
1899
- }
2090
+ try {
2091
+ hub = await Hub.open();
2092
+ } catch (err) {
2093
+ log("hub-unavailable", String(err?.message || err));
1900
2094
  }
1901
2095
  }
1902
2096
  const store = new SessionStore(stateDir(), { onFlush: hub ? (s) => hub.syncSession(s) : void 0 });
@@ -1909,6 +2103,8 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
1909
2103
  }
1910
2104
  }
1911
2105
  }
2106
+ const invites = new InviteStore(stateDir());
2107
+ const ownerToken = ownerKey(stateDir());
1912
2108
  const clients = /* @__PURE__ */ new Map();
1913
2109
  const polls = /* @__PURE__ */ new Map();
1914
2110
  const watchers = /* @__PURE__ */ new Map();
@@ -1929,14 +2125,44 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
1929
2125
  for (const h of listeners.keys()) extra.push(h);
1930
2126
  return allowedHosts(extra);
1931
2127
  }
1932
- function links(id) {
1933
- const local = `http://127.0.0.1:${port2}/s/${id}`;
1934
- const out = { local };
1935
- if (tailnet && listeners.has(tailnet.ip)) out.remote = `http://${tailnet.dnsName || tailnet.ip}:${port2}/s/${id}`;
2128
+ function remoteHost() {
1936
2129
  const linkHost = process.env.AUSTERE_AXI_LINK_HOST;
1937
- if (linkHost) out.remote = `http://${linkHost}:${port2}/s/${id}`;
2130
+ if (linkHost) return linkHost;
2131
+ if (tailnet && listeners.has(tailnet.ip)) return tailnet.dnsName || tailnet.ip;
2132
+ const explicit = process.env.AUSTERE_AXI_HOST;
2133
+ if (explicit && !/^(0\.0\.0\.0|::|\*)$/.test(explicit) && listeners.has(explicit)) return explicit;
2134
+ return null;
2135
+ }
2136
+ function remoteUrl(id) {
2137
+ const host = remoteHost();
2138
+ return host ? `http://${host}:${port2}/s/${id}` : null;
2139
+ }
2140
+ function links(id) {
2141
+ const out = { local: `http://127.0.0.1:${port2}/s/${id}` };
2142
+ const remote = remoteUrl(id);
2143
+ if (remote) out.remote = `${remote}?key=${ownerToken}`;
1938
2144
  return out;
1939
2145
  }
2146
+ function inviteLink(inv) {
2147
+ return `${remoteUrl(inv.sessionId)}?invite=${inv.token}`;
2148
+ }
2149
+ function isOwner(req) {
2150
+ if (isLoopback(req.socket.remoteAddress)) return true;
2151
+ const key = tokenFromCookie(req.headers.cookie, OWNER_COOKIE);
2152
+ return key != null && safeEqual(key, ownerToken);
2153
+ }
2154
+ function identify(req, s) {
2155
+ const token = tokenFromCookie(req.headers.cookie);
2156
+ if (token == null) return isOwner(req) ? OWNER : null;
2157
+ const inv = invites.find(token);
2158
+ if (!inv || inv.sessionId !== s.id) return null;
2159
+ return { role: "reviewer", name: inv.name, auto: inv.auto, token: inv.token };
2160
+ }
2161
+ function canSee(e, viewer) {
2162
+ if (viewer.role === "owner") return true;
2163
+ if (e.role === "user" && (e.held || e.discarded)) return e.author === viewer.name;
2164
+ return true;
2165
+ }
1940
2166
  function send(res, status, body, headers = {}) {
1941
2167
  const isText = typeof body === "string" || Buffer.isBuffer(body);
1942
2168
  const payload = isText ? body : JSON.stringify(body);
@@ -1985,11 +2211,32 @@ data: ${JSON.stringify(data)}
1985
2211
  broadcast(s.id, "presence", { presence: presence(s), status: s.status, endedBy: s.endedBy });
1986
2212
  }
1987
2213
  function broadcastTranscript(s, entries) {
1988
- broadcast(s.id, "transcript", { entries });
2214
+ const set2 = clients.get(s.id);
2215
+ if (!set2) return;
2216
+ for (const c of set2) {
2217
+ const visible = entries.filter((e) => canSee(e, c.viewer));
2218
+ if (visible.length) c.res.write(`event: transcript
2219
+ data: ${JSON.stringify({ entries: visible })}
2220
+
2221
+ `);
2222
+ }
2223
+ }
2224
+ function dropReviewers(tokens) {
2225
+ for (const set2 of clients.values()) {
2226
+ for (const c of Array.from(set2)) {
2227
+ if (c.viewer.role !== "reviewer" || !tokens.has(c.viewer.token)) continue;
2228
+ c.res.write(`event: server
2229
+ data: ${JSON.stringify({ state: "revoked" })}
2230
+
2231
+ `);
2232
+ c.res.end();
2233
+ set2.delete(c);
2234
+ }
2235
+ }
1989
2236
  }
1990
2237
  function readFileSafe(file2) {
1991
2238
  try {
1992
- return fs11.readFileSync(file2, "utf8");
2239
+ return fs12.readFileSync(file2, "utf8");
1993
2240
  } catch {
1994
2241
  return null;
1995
2242
  }
@@ -2001,14 +2248,14 @@ data: ${JSON.stringify(data)}
2001
2248
  const w = { html, program: art?.program ?? null, shell: art?.shell ?? null, timer: null };
2002
2249
  watchers.set(s.id, w);
2003
2250
  if (art?.title) s.title = art.title;
2004
- fs11.watchFile(s.file, { interval: 300 }, () => {
2251
+ fs12.watchFile(s.file, { interval: 300 }, () => {
2005
2252
  clearTimeout(w.timer);
2006
2253
  w.timer = setTimeout(() => onFileChange(s, w), 120);
2007
2254
  });
2008
2255
  }
2009
2256
  function unwatch(id) {
2010
2257
  const s = store.get(id);
2011
- if (s) fs11.unwatchFile(s.file);
2258
+ if (s) fs12.unwatchFile(s.file);
2012
2259
  watchers.delete(id);
2013
2260
  }
2014
2261
  function onFileChange(s, w) {
@@ -2050,7 +2297,7 @@ data: ${JSON.stringify(data)}
2050
2297
  const items = [];
2051
2298
  let end = false;
2052
2299
  for (const e of entries) {
2053
- for (const it2 of e.items || []) items.push(it2);
2300
+ for (const it2 of e.items || []) items.push(e.author ? { ...it2, author: e.author } : it2);
2054
2301
  if (e.end) end = true;
2055
2302
  }
2056
2303
  const body = {
@@ -2122,6 +2369,7 @@ data: ${JSON.stringify(data)}
2122
2369
  presence: presence(s2),
2123
2370
  browsers: clientCount(s2.id),
2124
2371
  pending: s2.pending.length,
2372
+ held: store.heldEntries(s2).length,
2125
2373
  links: links(s2.id)
2126
2374
  }))
2127
2375
  });
@@ -2131,12 +2379,52 @@ data: ${JSON.stringify(data)}
2131
2379
  setTimeout(() => shutdown("requested"), 50);
2132
2380
  return;
2133
2381
  }
2382
+ if (route === "share/list") {
2383
+ const withLinks = url2.searchParams.get("links") === "1";
2384
+ return send(res, 200, {
2385
+ invites: invites.list().map((i) => ({
2386
+ id: inviteId(i),
2387
+ name: i.name,
2388
+ file: i.file,
2389
+ auto: i.auto,
2390
+ createdAt: i.createdAt,
2391
+ link: withLinks ? inviteLink(i) : void 0
2392
+ }))
2393
+ });
2394
+ }
2134
2395
  if (req.method !== "POST") return send(res, 405, { error: "POST expected" });
2135
2396
  const body = await readBody(req);
2397
+ if (route === "share/revoke") {
2398
+ const gone = invites.revoke(String(body.key || ""));
2399
+ if (!gone.length) return send(res, 404, { error: "no invite matches that name or id" });
2400
+ dropReviewers(new Set(gone.map((i) => i.token)));
2401
+ log("share-revoke", gone.map((i) => `${i.name}(${inviteId(i)})`).join(","));
2402
+ return send(res, 200, { revoked: gone.map((i) => ({ id: inviteId(i), name: i.name, file: i.file })) });
2403
+ }
2136
2404
  const file2 = typeof body.file === "string" ? body.file : "";
2137
2405
  if (!file2) return send(res, 400, { error: "file is required" });
2406
+ if (route === "share") {
2407
+ if (!fs12.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
2408
+ await ensureTailnetListener();
2409
+ if (!remoteHost()) {
2410
+ return send(res, 409, {
2411
+ error: "no tailnet address: Tailscale is not running here and AUSTERE_AXI_HOST is not set, so a teammate could not open the link"
2412
+ });
2413
+ }
2414
+ let inv;
2415
+ try {
2416
+ const s2 = store.ensure(file2);
2417
+ inv = invites.add({ name: body.name, sessionId: s2.id, file: s2.file, auto: !!body.auto });
2418
+ watch(s2);
2419
+ store.touch(s2);
2420
+ } catch (err) {
2421
+ return send(res, 400, { error: String(err.message || err) });
2422
+ }
2423
+ log("share", inv.name, inviteId(inv), inv.auto ? "auto" : "hold");
2424
+ return send(res, 200, { id: inviteId(inv), name: inv.name, auto: inv.auto, link: inviteLink(inv), file: inv.file });
2425
+ }
2138
2426
  if (route === "open") {
2139
- if (!fs11.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
2427
+ if (!fs12.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
2140
2428
  await ensureTailnetListener();
2141
2429
  const s2 = store.ensure(file2);
2142
2430
  if (s2.status === "ended") {
@@ -2153,7 +2441,8 @@ data: ${JSON.stringify(data)}
2153
2441
  session: publicSession(s2),
2154
2442
  links: links(s2.id),
2155
2443
  browsers: clientCount(s2.id),
2156
- pending: s2.pending.length
2444
+ pending: s2.pending.length,
2445
+ held: store.heldEntries(s2).length
2157
2446
  });
2158
2447
  }
2159
2448
  const s = store.byFile(file2);
@@ -2246,9 +2535,9 @@ data: ${JSON.stringify(data)}
2246
2535
  }
2247
2536
  const file2 = installedPackFile(spec.name, spec.version);
2248
2537
  if (!file2) return send(res, 404, `pack ${spec.name}@${spec.version} is not installed (austere-axi components add ${spec.name}@${spec.version})`);
2249
- fs11.readFile(file2, (err, buf) => {
2538
+ fs12.readFile(file2, (err, buf) => {
2250
2539
  if (err) return send(res, 404, "not found");
2251
- const actual = `sha384-${crypto3.createHash("sha384").update(buf).digest("base64")}`;
2540
+ const actual = `sha384-${crypto4.createHash("sha384").update(buf).digest("base64")}`;
2252
2541
  const locked = readLock().packs[spec.name]?.integrity;
2253
2542
  const wanted = url2.searchParams.get("integrity");
2254
2543
  if (locked && locked !== actual) {
@@ -2270,23 +2559,23 @@ data: ${JSON.stringify(data)}
2270
2559
  }
2271
2560
  function serveAsset(res, rel, url2) {
2272
2561
  if (rel.startsWith("installed-packs/")) return serveInstalledPack(res, rel, url2);
2273
- const clean = path10.normalize(rel).replace(/^(\.\.[/\\])+/, "");
2274
- const file2 = path10.join(ASSET_DIR, clean);
2275
- if (!file2.startsWith(ASSET_DIR + path10.sep)) return send(res, 404, "not found");
2276
- fs11.stat(file2, (err, st2) => {
2562
+ const clean = path11.normalize(rel).replace(/^(\.\.[/\\])+/, "");
2563
+ const file2 = path11.join(ASSET_DIR, clean);
2564
+ if (!file2.startsWith(ASSET_DIR + path11.sep)) return send(res, 404, "not found");
2565
+ fs12.stat(file2, (err, st2) => {
2277
2566
  if (err || !st2.isFile()) return send(res, 404, "not found");
2278
2567
  res.writeHead(200, {
2279
- "content-type": MIME[path10.extname(file2)] || "application/octet-stream",
2568
+ "content-type": MIME[path11.extname(file2)] || "application/octet-stream",
2280
2569
  "content-length": st2.size,
2281
2570
  "cache-control": "no-cache",
2282
2571
  "x-content-type-options": "nosniff"
2283
2572
  });
2284
- fs11.createReadStream(file2).pipe(res);
2573
+ fs12.createReadStream(file2).pipe(res);
2285
2574
  });
2286
2575
  }
2287
2576
  function serveArtifact(req, res, s, rest) {
2288
- const dir = path10.dirname(s.file);
2289
- const base = path10.basename(s.file);
2577
+ const dir = path11.dirname(s.file);
2578
+ const base = path11.basename(s.file);
2290
2579
  if (rest === base || rest === "") {
2291
2580
  const html = readFileSafe(s.file);
2292
2581
  if (html == null) return send(res, 404, "artifact file is missing");
@@ -2299,28 +2588,29 @@ data: ${JSON.stringify(data)}
2299
2588
  } catch {
2300
2589
  return send(res, 400, "bad path");
2301
2590
  }
2302
- const file2 = path10.resolve(dir, decoded);
2303
- if (!file2.startsWith(dir + path10.sep) || decoded.split(/[/\\]/).some((p) => p.startsWith("."))) {
2591
+ const file2 = path11.resolve(dir, decoded);
2592
+ if (!file2.startsWith(dir + path11.sep) || decoded.split(/[/\\]/).some((p) => p.startsWith("."))) {
2304
2593
  return send(res, 404, "not found");
2305
2594
  }
2306
- fs11.stat(file2, (err, st2) => {
2595
+ fs12.stat(file2, (err, st2) => {
2307
2596
  if (err || !st2.isFile() || st2.size > 64 * 1024 * 1024) return send(res, 404, "not found");
2308
2597
  res.writeHead(200, {
2309
- "content-type": MIME[path10.extname(file2).toLowerCase()] || "application/octet-stream",
2598
+ "content-type": MIME[path11.extname(file2).toLowerCase()] || "application/octet-stream",
2310
2599
  "content-length": st2.size,
2311
2600
  "cache-control": "no-cache",
2312
2601
  "x-content-type-options": "nosniff"
2313
2602
  });
2314
- fs11.createReadStream(file2).pipe(res);
2603
+ fs12.createReadStream(file2).pipe(res);
2315
2604
  });
2316
2605
  }
2317
- function chromePage(s) {
2606
+ function chromePage(s, viewer) {
2318
2607
  const boot = {
2319
2608
  id: s.id,
2320
2609
  title: s.title,
2321
- file: s.file,
2322
- name: path10.basename(s.file),
2323
- artifactUrl: `/s/${s.id}/a/${encodeURIComponent(path10.basename(s.file))}`,
2610
+ file: viewer.role === "owner" ? s.file : "",
2611
+ viewer: { role: viewer.role, name: viewer.name, auto: viewer.role === "reviewer" ? viewer.auto : void 0 },
2612
+ name: path11.basename(s.file),
2613
+ artifactUrl: `/s/${s.id}/a/${encodeURIComponent(path11.basename(s.file))}`,
2324
2614
  version: VERSION
2325
2615
  };
2326
2616
  return `<!doctype html>
@@ -2343,10 +2633,20 @@ data: ${JSON.stringify(data)}
2343
2633
  const project = (url2.searchParams.get("project") || "").slice(0, 1e3);
2344
2634
  return hubPage(hub.list({ q: q2, project }), { version: VERSION, q: q2, project, stats: hub.stats() });
2345
2635
  }
2636
+ function hubNotes(url2) {
2637
+ store.flushAll();
2638
+ const q2 = (url2.searchParams.get("q") || "").slice(0, 200);
2639
+ const project = (url2.searchParams.get("project") || "").slice(0, 1e3);
2640
+ const st2 = url2.searchParams.get("status");
2641
+ const status = st2 === "unanswered" || st2 === "answered" ? st2 : "all";
2642
+ const author = (url2.searchParams.get("author") || "").slice(0, 80);
2643
+ const rows = hub.notes({ q: q2, project, author, status: status === "all" ? "" : status });
2644
+ return notesPage(rows, { version: VERSION, q: q2, project, status, author, authors: hub.authors(), stats: hub.stats() });
2645
+ }
2346
2646
  function hubOpen(res, id) {
2347
2647
  const a = /^[a-f0-9]{12}$/.test(id) ? hub.artifact(id) : null;
2348
2648
  if (!a) return send(res, 404, "not in the hub");
2349
- if (!fs11.existsSync(a.file)) return send(res, 410, `${a.file} no longer exists`);
2649
+ if (!fs12.existsSync(a.file)) return send(res, 410, `${a.file} no longer exists`);
2350
2650
  const existed = store.byFile(a.file);
2351
2651
  const s = store.ensure(a.file);
2352
2652
  if (!existed) store.touch(s);
@@ -2369,9 +2669,23 @@ data: ${JSON.stringify(data)}
2369
2669
  }
2370
2670
  if (url2.pathname.startsWith("/api/")) return handleApi(req, res, url2);
2371
2671
  if (url2.pathname.startsWith("/__austere/")) return serveAsset(res, url2.pathname.slice("/__austere/".length), url2);
2672
+ const key = url2.searchParams.get("key");
2673
+ if (key != null) {
2674
+ if (!safeEqual(key, ownerToken)) return send(res, 403, "This link is not valid.");
2675
+ url2.searchParams.delete("key");
2676
+ res.writeHead(302, {
2677
+ location: url2.pathname + url2.search,
2678
+ "set-cookie": `${OWNER_COOKIE}=${ownerToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`,
2679
+ "cache-control": "no-store"
2680
+ });
2681
+ return res.end();
2682
+ }
2683
+ const ownerOnly = url2.pathname === "/" || url2.pathname === "/notes" || url2.pathname.startsWith("/hub/");
2684
+ if (ownerOnly && !isOwner(req)) return send(res, 403, "Only the owner can open this. Ask the owner for a link to the page.");
2372
2685
  if (url2.pathname === "/") {
2373
2686
  return send(res, 200, hub ? hubHome(url2) : homePage(), { "content-type": "text/html; charset=utf-8" });
2374
2687
  }
2688
+ if (hub && url2.pathname === "/notes") return send(res, 200, hubNotes(url2), { "content-type": "text/html; charset=utf-8" });
2375
2689
  if (hub && url2.pathname.startsWith("/hub/open/")) return hubOpen(res, url2.pathname.slice("/hub/open/".length));
2376
2690
  if (url2.pathname === "/favicon.ico") return send(res, 204, "");
2377
2691
  const m = /^\/s\/([a-f0-9]{12})(\/.*)?$/.exec(url2.pathname);
@@ -2380,9 +2694,31 @@ data: ${JSON.stringify(data)}
2380
2694
  if (!s) return send(res, 404, "no such session");
2381
2695
  const sub = m[2] || "";
2382
2696
  activity();
2697
+ const invite = url2.searchParams.get("invite");
2698
+ if (invite != null && (sub === "" || sub === "/")) {
2699
+ const inv = invites.find(invite);
2700
+ if (!inv || inv.sessionId !== s.id) return send(res, 403, "This invite link is not valid. It may have been revoked; ask for a new one.");
2701
+ res.writeHead(302, {
2702
+ location: `/s/${s.id}`,
2703
+ "set-cookie": `${COOKIE}=${inv.token}; Path=/s/${s.id}; HttpOnly; SameSite=Lax; Max-Age=2592000`,
2704
+ "cache-control": "no-store"
2705
+ });
2706
+ return res.end();
2707
+ }
2708
+ const who = identify(req, s);
2709
+ if (!who) {
2710
+ if (tokenFromCookie(req.headers.cookie) == null) return send(res, 403, "Ask the owner for a link to this page.");
2711
+ return send(res, 403, "This invite is no longer valid. Ask for a new link.", {
2712
+ "set-cookie": `${COOKIE}=; Path=/s/${s.id}; HttpOnly; SameSite=Lax; Max-Age=0`
2713
+ });
2714
+ }
2715
+ const reviewer = who.role === "reviewer";
2716
+ if (reviewer && !(sub === "" || sub === "/" || sub.startsWith("/a/") || sub === "/events" || sub === "/send" || sub === "/bye")) {
2717
+ return send(res, 403, { error: "reviewers can only read the page and add notes" });
2718
+ }
2383
2719
  if (sub === "" || sub === "/") {
2384
2720
  watch(s);
2385
- return send(res, 200, chromePage(s), { "content-type": "text/html; charset=utf-8" });
2721
+ return send(res, 200, chromePage(s, who), { "content-type": "text/html; charset=utf-8" });
2386
2722
  }
2387
2723
  if (sub.startsWith("/a/")) return serveArtifact(req, res, s, sub.slice(3));
2388
2724
  if (sub === "/events") {
@@ -2396,20 +2732,20 @@ data: ${JSON.stringify(data)}
2396
2732
  res.write("retry: 2000\n\n");
2397
2733
  const w = watchers.get(s.id);
2398
2734
  const snapshot = {
2399
- session: publicSession(s),
2735
+ session: reviewer ? { ...publicSession(s), file: "" } : publicSession(s),
2400
2736
  presence: presence(s),
2401
- transcript: s.transcript.slice(-200),
2737
+ transcript: s.transcript.filter((e) => !e.discarded && canSee(e, who)).slice(-200),
2402
2738
  revision: s.revision,
2403
2739
  program: w?.program ?? null,
2404
2740
  thirdPartyPacks: thirdPartyPacks(w?.html),
2405
2741
  version: VERSION,
2406
- links: links(s.id)
2742
+ links: reviewer ? null : links(s.id)
2407
2743
  };
2408
2744
  res.write(`event: snapshot
2409
2745
  data: ${JSON.stringify(snapshot)}
2410
2746
 
2411
2747
  `);
2412
- const client = { res };
2748
+ const client = { res, viewer: who };
2413
2749
  if (!clients.has(s.id)) clients.set(s.id, /* @__PURE__ */ new Set());
2414
2750
  clients.get(s.id).add(client);
2415
2751
  browserSeen.add(s.id);
@@ -2426,13 +2762,19 @@ data: ${JSON.stringify(snapshot)}
2426
2762
  if (req.method === "POST" && !checkOrigin(req)) return send(res, 403, { error: "cross-origin request refused" });
2427
2763
  if (sub === "/send" && req.method === "POST") {
2428
2764
  const body = await readBody(req, 4 * 1024 * 1024);
2429
- const items = Array.isArray(body.items) ? body.items.filter((x) => x && typeof x === "object").slice(0, 200) : [];
2765
+ const end = !reviewer && !!body.end;
2766
+ let items = Array.isArray(body.items) ? body.items.filter((x) => x && typeof x === "object").slice(0, reviewer ? 50 : 200) : [];
2767
+ if (reviewer) items = items.filter((x) => REVIEWER_KINDS.has(x.kind));
2430
2768
  if (s.status === "ended") return send(res, 409, { error: "session has ended" });
2431
- if (!items.length && !body.end) return send(res, 400, { error: "nothing to send" });
2432
- for (const it2 of items) it2.id = it2.id || newId("i");
2769
+ if (!items.length && !end) return send(res, 400, { error: "nothing to send" });
2770
+ for (const it2 of items) {
2771
+ delete it2.author;
2772
+ it2.id = reviewer || !it2.id ? newId("i") : it2.id;
2773
+ }
2774
+ const hold = reviewer && !who.auto;
2433
2775
  let entry = null;
2434
- if (items.length) entry = store.addUserBatch(s, items, { end: !!body.end });
2435
- if (body.end) {
2776
+ if (items.length) entry = store.addUserBatch(s, items, { end, author: who.name, hold });
2777
+ if (end) {
2436
2778
  store.end(s, "user");
2437
2779
  broadcast(s.id, "ended", { by: "user" });
2438
2780
  }
@@ -2440,11 +2782,21 @@ data: ${JSON.stringify(snapshot)}
2440
2782
  const poll = polls.get(s.id);
2441
2783
  if (poll && !poll.done) {
2442
2784
  if (s.pending.length) deliver(s, poll);
2443
- else if (body.end) finishPoll(s, poll, { status: "ended", endedBy: "user" });
2785
+ else if (end) finishPoll(s, poll, { status: "ended", endedBy: "user" });
2444
2786
  }
2445
2787
  broadcastPresence(s);
2446
2788
  return send(res, 200, { ok: true, entry });
2447
2789
  }
2790
+ if ((sub === "/release" || sub === "/discard") && req.method === "POST") {
2791
+ const body = await readBody(req);
2792
+ const ids = Array.isArray(body.ids) ? body.ids.map(String) : body.all ? null : [];
2793
+ const done = sub === "/release" ? store.release(s, ids) : store.discard(s, ids);
2794
+ if (done.length) broadcastTranscript(s, done);
2795
+ const poll = polls.get(s.id);
2796
+ if (sub === "/release" && poll && !poll.done && s.pending.length) deliver(s, poll);
2797
+ broadcastPresence(s);
2798
+ return send(res, 200, { ok: true, count: done.length });
2799
+ }
2448
2800
  if (sub === "/end" && req.method === "POST") {
2449
2801
  if (s.status !== "ended") {
2450
2802
  store.end(s, "user");
@@ -2467,7 +2819,7 @@ data: ${JSON.stringify(snapshot)}
2467
2819
  if (html == null) return send(res, 404, "artifact file is missing");
2468
2820
  try {
2469
2821
  const out = exportHtml(html);
2470
- const name = path10.basename(s.file).replace(/\.html?$/i, "") + ".export.html";
2822
+ const name = path11.basename(s.file).replace(/\.html?$/i, "") + ".export.html";
2471
2823
  return send(res, 200, out.html, {
2472
2824
  "content-type": "text/html; charset=utf-8",
2473
2825
  "content-disposition": `attachment; filename="${name.replace(/"/g, "")}"`
@@ -2483,7 +2835,7 @@ data: ${JSON.stringify(snapshot)}
2483
2835
  if (listeners.has(host)) return resolve(true);
2484
2836
  const server = http2.createServer((req, res) => {
2485
2837
  handle(req, res).catch((err) => {
2486
- log("error", req.method, req.url, String(err?.stack || err));
2838
+ log("error", req.method, req.url.split("?")[0], String(err?.stack || err));
2487
2839
  if (!res.headersSent) send(res, err.status || 500, { error: String(err.message || err) });
2488
2840
  else res.end();
2489
2841
  });
@@ -2492,7 +2844,12 @@ data: ${JSON.stringify(snapshot)}
2492
2844
  server.requestTimeout = 0;
2493
2845
  server.headersTimeout = 6e4;
2494
2846
  server.once("error", (err) => {
2495
- log("listen-failed", host, err.code || err.message);
2847
+ log(
2848
+ "listen-failed",
2849
+ host,
2850
+ /** @type {NodeJS.ErrnoException} */
2851
+ err.code || err.message
2852
+ );
2496
2853
  resolve(false);
2497
2854
  });
2498
2855
  server.listen(port2, host, () => {
@@ -2551,7 +2908,7 @@ data: ${JSON.stringify({ state: "stopped", reason })}
2551
2908
  log("started", `v${VERSION}`, `pid=${process.pid}`);
2552
2909
  return { port: port2, shutdown };
2553
2910
  }
2554
- var MIME, SHORT_GRACE_MS;
2911
+ var MIME, SHORT_GRACE_MS, OWNER, REVIEWER_KINDS;
2555
2912
  var init_server = __esm({
2556
2913
  "src/cli/server.js"() {
2557
2914
  init_artifact();
@@ -2561,6 +2918,7 @@ var init_server = __esm({
2561
2918
  init_sessions();
2562
2919
  init_packs();
2563
2920
  init_hub();
2921
+ init_invites();
2564
2922
  init_hub_page();
2565
2923
  init_packs_meta();
2566
2924
  MIME = {
@@ -2590,12 +2948,14 @@ var init_server = __esm({
2590
2948
  ".webm": "video/webm"
2591
2949
  };
2592
2950
  SHORT_GRACE_MS = 1e4;
2951
+ OWNER = Object.freeze({ role: "owner", name: "" });
2952
+ REVIEWER_KINDS = /* @__PURE__ */ new Set(["annotation", "message"]);
2593
2953
  }
2594
2954
  });
2595
2955
 
2596
2956
  // src/cli/main.js
2597
- import fs12 from "node:fs";
2598
- import path11 from "node:path";
2957
+ import fs13 from "node:fs";
2958
+ import path12 from "node:path";
2599
2959
 
2600
2960
  // node_modules/@openuidev/lang-core/dist/shared-Dzgc28Wo.mjs
2601
2961
  var TELEMETRY_REQUEST_TIMEOUT_MS = 2e3;
@@ -3456,10 +3816,10 @@ function mergeDefs(...defs) {
3456
3816
  function cloneDef(schema) {
3457
3817
  return mergeDefs(schema._zod.def);
3458
3818
  }
3459
- function getElementAtPath(obj, path12) {
3460
- if (!path12)
3819
+ function getElementAtPath(obj, path13) {
3820
+ if (!path13)
3461
3821
  return obj;
3462
- return path12.reduce((acc, key) => acc?.[key], obj);
3822
+ return path13.reduce((acc, key) => acc?.[key], obj);
3463
3823
  }
3464
3824
  function promiseAllObject(promisesObj) {
3465
3825
  const keys = Object.keys(promisesObj);
@@ -3799,11 +4159,11 @@ function explicitlyAborted(x, startIndex = 0) {
3799
4159
  }
3800
4160
  return false;
3801
4161
  }
3802
- function prefixIssues(path12, issues) {
4162
+ function prefixIssues(path13, issues) {
3803
4163
  return issues.map((iss) => {
3804
4164
  var _a3;
3805
4165
  (_a3 = iss).path ?? (_a3.path = []);
3806
- iss.path.unshift(path12);
4166
+ iss.path.unshift(path13);
3807
4167
  return iss;
3808
4168
  });
3809
4169
  }
@@ -4265,16 +4625,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
4265
4625
  }
4266
4626
  function formatError(error62, mapper = (issue2) => issue2.message) {
4267
4627
  const fieldErrors = { _errors: [] };
4268
- const processError = (error63, path12 = []) => {
4628
+ const processError = (error63, path13 = []) => {
4269
4629
  for (const issue2 of error63.issues) {
4270
4630
  if (issue2.code === "invalid_union" && issue2.errors.length) {
4271
- issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
4631
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
4272
4632
  } else if (issue2.code === "invalid_key") {
4273
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4633
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4274
4634
  } else if (issue2.code === "invalid_element") {
4275
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4635
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4276
4636
  } else {
4277
- const fullpath = [...path12, ...issue2.path];
4637
+ const fullpath = [...path13, ...issue2.path];
4278
4638
  if (fullpath.length === 0) {
4279
4639
  fieldErrors._errors.push(mapper(issue2));
4280
4640
  } else {
@@ -4313,17 +4673,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
4313
4673
  }
4314
4674
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
4315
4675
  const result = { errors: [] };
4316
- const processError = (error63, path12 = []) => {
4676
+ const processError = (error63, path13 = []) => {
4317
4677
  var _a3;
4318
4678
  for (const issue2 of error63.issues) {
4319
4679
  if (issue2.code === "invalid_union" && issue2.errors.length) {
4320
- issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
4680
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
4321
4681
  } else if (issue2.code === "invalid_key") {
4322
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4682
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4323
4683
  } else if (issue2.code === "invalid_element") {
4324
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4684
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4325
4685
  } else {
4326
- const fullpath = [...path12, ...issue2.path];
4686
+ const fullpath = [...path13, ...issue2.path];
4327
4687
  if (fullpath.length === 0) {
4328
4688
  result.errors.push(mapper(issue2));
4329
4689
  continue;
@@ -4362,8 +4722,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
4362
4722
  }
4363
4723
  function toDotPath(_path) {
4364
4724
  const segs = [];
4365
- const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4366
- for (const seg of path12) {
4725
+ const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4726
+ for (const seg of path13) {
4367
4727
  if (typeof seg === "number")
4368
4728
  segs.push(`[${seg}]`);
4369
4729
  else if (typeof seg === "symbol")
@@ -21501,13 +21861,13 @@ function resolveRef(ref, ctx) {
21501
21861
  if (!ref.startsWith("#")) {
21502
21862
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
21503
21863
  }
21504
- const path12 = ref.slice(1).split("/").filter(Boolean);
21505
- if (path12.length === 0) {
21864
+ const path13 = ref.slice(1).split("/").filter(Boolean);
21865
+ if (path13.length === 0) {
21506
21866
  return ctx.rootSchema;
21507
21867
  }
21508
21868
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
21509
- if (path12[0] === defsKey) {
21510
- const key = path12[1] === void 0 ? void 0 : decodeJSONPointerSegment(path12[1]);
21869
+ if (path13[0] === defsKey) {
21870
+ const key = path13[1] === void 0 ? void 0 : decodeJSONPointerSegment(path13[1]);
21511
21871
  if (!key || !ctx.defs[key]) {
21512
21872
  throw new Error(`Reference not found: ${ref}`);
21513
21873
  }
@@ -22389,11 +22749,11 @@ function defineAction(config2) {
22389
22749
  ref: actionRef(config2.name, data)
22390
22750
  };
22391
22751
  }
22392
- function resolveField(obj, path12) {
22393
- if (!path12 || obj == null) return void 0;
22394
- if (!path12.includes(".")) return obj[path12];
22752
+ function resolveField(obj, path13) {
22753
+ if (!path13 || obj == null) return void 0;
22754
+ if (!path13.includes(".")) return obj[path13];
22395
22755
  let cur = obj;
22396
- for (const p of path12.split(".")) {
22756
+ for (const p of path13.split(".")) {
22397
22757
  if (cur == null) return void 0;
22398
22758
  cur = cur[p];
22399
22759
  }
@@ -23769,14 +24129,14 @@ function checkTypeMismatch(value, info) {
23769
24129
  };
23770
24130
  return null;
23771
24131
  }
23772
- function validationMessage(component, path12, issue2) {
24132
+ function validationMessage(component, path13, issue2) {
23773
24133
  switch (issue2.code) {
23774
24134
  case "type-mismatch":
23775
- return `field "${path12}" expects ${issue2.expected} but got ${issue2.actual}`;
24135
+ return `field "${path13}" expects ${issue2.expected} but got ${issue2.actual}`;
23776
24136
  case "missing-required":
23777
- return `missing required field "${path12}"${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
24137
+ return `missing required field "${path13}"${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
23778
24138
  case "null-required":
23779
- return `required field "${path12}" cannot be null${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
24139
+ return `required field "${path13}" cannot be null${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
23780
24140
  case "unknown-component":
23781
24141
  return `Unknown component "${component}" \u2014 not found in catalog or builtins${issue2.available?.length ? `. Available components: ${issue2.available.join(", ")}` : ""}`;
23782
24142
  case "inline-reserved":
@@ -23791,12 +24151,12 @@ function validationMessage(component, path12, issue2) {
23791
24151
  return component ? `No root statement, and no statement calls the root component ${component}` : "No root statement";
23792
24152
  }
23793
24153
  }
23794
- function pushValidationIssue(ctx, component, path12, issue2) {
24154
+ function pushValidationIssue(ctx, component, path13, issue2) {
23795
24155
  ctx.errors.push({
23796
24156
  code: issue2.code,
23797
24157
  component,
23798
- path: path12,
23799
- message: validationMessage(component, path12, issue2),
24158
+ path: path13,
24159
+ message: validationMessage(component, path13, issue2),
23800
24160
  statementId: ctx.currentStatementId,
23801
24161
  ...issue2.code === "no-root" && issue2.reason === "fallback" && { severity: "warning" }
23802
24162
  });
@@ -23806,10 +24166,10 @@ function resolveInvalidValue(required2, defaultValue) {
23806
24166
  if (defaultValue !== void 0) return defaultValue;
23807
24167
  return required2 ? INVALID2 : void 0;
23808
24168
  }
23809
- function validateElementPosition(element, s, component, path12, ctx) {
24169
+ function validateElementPosition(element, s, component, path13, ctx) {
23810
24170
  if (isCompositeSchema(s)) return false;
23811
24171
  if (typeof s["type"] === "string" || Array.isArray(s["enum"]) || "const" in s) {
23812
- pushValidationIssue(ctx, component, path12, {
24172
+ pushValidationIssue(ctx, component, path13, {
23813
24173
  code: "type-mismatch",
23814
24174
  expected: typeof s["type"] === "string" ? s["type"] : "a literal value",
23815
24175
  actual: `component "${element.typeName}"`
@@ -23825,9 +24185,9 @@ function isOnlyComponentSlot(s, ctx) {
23825
24185
  const names = slotRefNames(s);
23826
24186
  return names.length > 0 && names.every((n) => n !== void 0 && ctx.cat?.has(n));
23827
24187
  }
23828
- function validateObjectValue(value, s, component, path12, ctx) {
24188
+ function validateObjectValue(value, s, component, path13, ctx) {
23829
24189
  if (typeof value !== "object" || Array.isArray(value)) {
23830
- pushValidationIssue(ctx, component, path12, {
24190
+ pushValidationIssue(ctx, component, path13, {
23831
24191
  code: "type-mismatch",
23832
24192
  expected: "object",
23833
24193
  actual: jsType(value)
@@ -23847,14 +24207,14 @@ function validateObjectValue(value, s, component, path12, ctx) {
23847
24207
  (out ??= { ...obj })[key] = fallback;
23848
24208
  continue;
23849
24209
  }
23850
- pushValidationIssue(ctx, component, `${path12}/${key}`, { code: present ? "null-required" : "missing-required" });
24210
+ pushValidationIssue(ctx, component, `${path13}/${key}`, { code: present ? "null-required" : "missing-required" });
23851
24211
  invalid = true;
23852
24212
  }
23853
24213
  }
23854
24214
  for (const key of Object.keys(props)) {
23855
24215
  if (!(key in obj)) continue;
23856
24216
  const sub = props[key];
23857
- let next = validateSchemaValue(obj[key], sub, component, `${path12}/${key}`, ctx);
24217
+ let next = validateSchemaValue(obj[key], sub, component, `${path13}/${key}`, ctx);
23858
24218
  if (next === INVALID2) {
23859
24219
  next = resolveInvalidValue(required2.includes(key), getSchemaDefaultValue(sub));
23860
24220
  if (next === INVALID2) {
@@ -23869,9 +24229,9 @@ function validateObjectValue(value, s, component, path12, ctx) {
23869
24229
  }
23870
24230
  return invalid ? INVALID2 : out ?? obj;
23871
24231
  }
23872
- function validateArrayValue(value, s, component, path12, ctx) {
24232
+ function validateArrayValue(value, s, component, path13, ctx) {
23873
24233
  if (!Array.isArray(value)) {
23874
- pushValidationIssue(ctx, component, path12, {
24234
+ pushValidationIssue(ctx, component, path13, {
23875
24235
  code: "type-mismatch",
23876
24236
  expected: "array",
23877
24237
  actual: jsType(value)
@@ -23882,19 +24242,19 @@ function validateArrayValue(value, s, component, path12, ctx) {
23882
24242
  if (!items || typeof items !== "object" || Array.isArray(items)) return value;
23883
24243
  let out;
23884
24244
  for (let i = 0; i < value.length; i++) {
23885
- const next = validateSchemaValue(value[i], items, component, `${path12}/${i}`, ctx);
24245
+ const next = validateSchemaValue(value[i], items, component, `${path13}/${i}`, ctx);
23886
24246
  if (next !== value[i] && !out) out = value.slice(0, i);
23887
24247
  if (out && next !== INVALID2) out.push(next);
23888
24248
  }
23889
24249
  return out ?? value;
23890
24250
  }
23891
- function validateLeafValue(value, s, component, path12, ctx) {
24251
+ function validateLeafValue(value, s, component, path13, ctx) {
23892
24252
  const leaf = getScalarTypeInfo(s);
23893
24253
  if (leaf.expectedType == null && leaf.enumValues == null) return false;
23894
24254
  if (leaf.enumValues && ctx.partial) return false;
23895
24255
  const mismatch = checkTypeMismatch(value, leaf);
23896
24256
  if (mismatch) {
23897
- pushValidationIssue(ctx, component, path12, {
24257
+ pushValidationIssue(ctx, component, path13, {
23898
24258
  code: "type-mismatch",
23899
24259
  ...mismatch
23900
24260
  });
@@ -23902,15 +24262,15 @@ function validateLeafValue(value, s, component, path12, ctx) {
23902
24262
  }
23903
24263
  return false;
23904
24264
  }
23905
- function validateSchemaValue(value, schema, component, path12, ctx) {
24265
+ function validateSchemaValue(value, schema, component, path13, ctx) {
23906
24266
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) return value;
23907
24267
  const s = schema;
23908
24268
  if (value == null) return value;
23909
24269
  if (isASTNode(value)) return value;
23910
- if (isElementNode(value)) return validateElementPosition(value, s, component, path12, ctx) ? INVALID2 : value;
24270
+ if (isElementNode(value)) return validateElementPosition(value, s, component, path13, ctx) ? INVALID2 : value;
23911
24271
  if (isCompositeSchema(s)) {
23912
24272
  if (!isOnlyComponentSlot(s, ctx)) return value;
23913
- pushValidationIssue(ctx, component, path12, {
24273
+ pushValidationIssue(ctx, component, path13, {
23914
24274
  code: "type-mismatch",
23915
24275
  expected: slotRefNames(s).join(" | "),
23916
24276
  actual: Array.isArray(value) ? "array" : typeof value === "object" ? "plain object" : typeof value
@@ -23918,9 +24278,9 @@ function validateSchemaValue(value, schema, component, path12, ctx) {
23918
24278
  return INVALID2;
23919
24279
  }
23920
24280
  const type = s["type"];
23921
- if (type === "object") return validateObjectValue(value, s, component, path12, ctx);
23922
- if (type === "array") return validateArrayValue(value, s, component, path12, ctx);
23923
- return validateLeafValue(value, s, component, path12, ctx) ? INVALID2 : value;
24281
+ if (type === "object") return validateObjectValue(value, s, component, path13, ctx);
24282
+ if (type === "array") return validateArrayValue(value, s, component, path13, ctx);
24283
+ return validateLeafValue(value, s, component, path13, ctx) ? INVALID2 : value;
23924
24284
  }
23925
24285
  function containsDynamicValue(v) {
23926
24286
  if (v == null || typeof v !== "object") return false;
@@ -27542,11 +27902,11 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
27542
27902
  const blocked = solid.some(([id, r]) => id !== e.from && id !== e.to && seg.x0 < r.x + r.width && r.x < seg.x1 && seg.y0 < r.y + r.height && r.y < seg.y1);
27543
27903
  if (!blocked) {
27544
27904
  routes.set(e.id, [a, b]);
27545
- const path13 = [];
27905
+ const path14 = [];
27546
27906
  const [i0, j0, i1, j1] = [ix(a.x), iy(a.y), ix(b.x), iy(b.y)];
27547
- if (j0 === j1) for (let i = Math.min(i0, i1); i <= Math.max(i0, i1); i++) path13.push([i, j0]);
27548
- else for (let j = Math.min(j0, j1); j <= Math.max(j0, j1); j++) path13.push([i0, j]);
27549
- if (path13.every(([i, j]) => i !== void 0 && j !== void 0)) mark(path13);
27907
+ if (j0 === j1) for (let i = Math.min(i0, i1); i <= Math.max(i0, i1); i++) path14.push([i, j0]);
27908
+ else for (let j = Math.min(j0, j1); j <= Math.max(j0, j1); j++) path14.push([i0, j]);
27909
+ if (path14.every(([i, j]) => i !== void 0 && j !== void 0)) mark(path14);
27550
27910
  continue;
27551
27911
  }
27552
27912
  }
@@ -27556,12 +27916,12 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
27556
27916
  const sj = iy(st2.y);
27557
27917
  const ti = ix(tt2.x);
27558
27918
  const tj = iy(tt2.y);
27559
- let path12 = null;
27919
+ let path13 = null;
27560
27920
  if (si !== void 0 && sj !== void 0 && ti !== void 0 && tj !== void 0) {
27561
- path12 = search(si, sj, dirOf(e.fromSide), ti, tj, reverse[dirOf(e.toSide)]);
27921
+ path13 = search(si, sj, dirOf(e.fromSide), ti, tj, reverse[dirOf(e.toSide)]);
27562
27922
  }
27563
- const pts = path12 ? path12.map(([i, j]) => ({ x: xs[i], y: ys[j] })) : elbow(st2, tt2, e.fromSide);
27564
- if (path12) mark(path12);
27923
+ const pts = path13 ? path13.map(([i, j]) => ({ x: xs[i], y: ys[j] })) : elbow(st2, tt2, e.fromSide);
27924
+ if (path13) mark(path13);
27565
27925
  routes.set(e.id, simplify([e.fromPort, ...pts, e.toPort]));
27566
27926
  }
27567
27927
  return routes;
@@ -27630,10 +27990,10 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
27630
27990
  }
27631
27991
  return out.reverse();
27632
27992
  }
27633
- function mark(path12) {
27634
- for (let k = 1; k < path12.length; k++) {
27635
- const [i0, j0] = path12[k - 1];
27636
- const [i1, j1] = path12[k];
27993
+ function mark(path13) {
27994
+ for (let k = 1; k < path13.length; k++) {
27995
+ const [i0, j0] = path13[k - 1];
27996
+ const [i1, j1] = path13[k];
27637
27997
  if (j0 === j1) {
27638
27998
  hUse[j0 * nx + Math.min(i0, i1)]++;
27639
27999
  hPass[j0 * nx + i0]++;
@@ -28320,7 +28680,7 @@ import fs5 from "node:fs";
28320
28680
  import http from "node:http";
28321
28681
  import path4 from "node:path";
28322
28682
  function wantHub() {
28323
- return hubEnabled() && !hubUnsupported();
28683
+ return hubEnabled();
28324
28684
  }
28325
28685
  function request(method, route, body, { timeoutMs = 0, port: port2 = port() } = {}) {
28326
28686
  return new Promise((resolve, reject) => {
@@ -28379,11 +28739,16 @@ function spawnServer(port2, alsoListen = []) {
28379
28739
  env: { ...process.env, OPENUI_TELEMETRY_DISABLED: "1" },
28380
28740
  windowsHide: true
28381
28741
  });
28742
+ let exited = null;
28743
+ child.on("exit", (code) => exited = code ?? "signal");
28382
28744
  child.unref();
28745
+ return () => exited;
28383
28746
  }
28747
+ var START_TIMEOUT_MS = 3e4;
28384
28748
  async function ensureServer() {
28385
28749
  const port2 = port();
28386
28750
  let h = await health(port2);
28751
+ let exited = () => null;
28387
28752
  if (h?.foreign) {
28388
28753
  throw new Error(`port ${port2} is in use by something that is not austere-axi; set AUSTERE_AXI_PORT to another port`);
28389
28754
  }
@@ -28396,15 +28761,21 @@ async function ensureServer() {
28396
28761
  });
28397
28762
  for (let i = 0; i < 40 && await health(port2); i++) await sleep(100);
28398
28763
  h = null;
28399
- spawnServer(port2, keep);
28764
+ exited = spawnServer(port2, keep);
28400
28765
  } else if (!h) {
28401
- spawnServer(port2);
28766
+ exited = spawnServer(port2);
28402
28767
  }
28403
28768
  if (!h) {
28404
- for (let i = 0; i < 80; i++) {
28769
+ const deadline = Date.now() + START_TIMEOUT_MS;
28770
+ while (Date.now() < deadline) {
28405
28771
  await sleep(100);
28406
28772
  h = await health(port2);
28407
28773
  if (h && !h.foreign) return h;
28774
+ if (exited() != null) {
28775
+ h = await health(port2);
28776
+ if (h && !h.foreign) return h;
28777
+ break;
28778
+ }
28408
28779
  }
28409
28780
  throw new Error(`server did not start on port ${port2}; see ${path4.join(stateDir(), "server.log")}`);
28410
28781
  }
@@ -28441,6 +28812,7 @@ function openBrowser(url2) {
28441
28812
 
28442
28813
  // src/cli/main.js
28443
28814
  init_hub();
28815
+ init_invites();
28444
28816
  init_sessions();
28445
28817
  init_export();
28446
28818
 
@@ -28689,7 +29061,10 @@ COMMANDS
28689
29061
  (check --fix writes the <meta name="austere-packs"> tag)
28690
29062
  ${BIN2} components new <dir> | build [dir] write your own pack
28691
29063
  ${BIN2} export <file> single self-contained HTML [--out path]
28692
- ${BIN2} hub on|off|status|list|open keep the server up, indexing every artifact by project/date
29064
+ ${BIN2} hub on|off|status|list|notes|open keep the server up, indexing every artifact by project/date
29065
+ ${BIN2} share <file> --as "<name>" [--auto] invite link for a teammate on your tailnet. Their notes
29066
+ wait for the owner to send them on; --auto skips the wait
29067
+ ${BIN2} share list [--links] | revoke <name|id>
28693
29068
  ${BIN2} stop stop the background server
28694
29069
 
28695
29070
  ENV
@@ -28709,7 +29084,7 @@ function parseArgs(argv) {
28709
29084
  if (a.startsWith("--")) {
28710
29085
  const eq = a.indexOf("=");
28711
29086
  const key = eq > 0 ? a.slice(2, eq) : a.slice(2);
28712
- const takesValue = ["agent-reply", "agent-reply-file", "owner", "out", "port", "also-listen", "timeout-ms", "name", "project"].includes(key);
29087
+ const takesValue = ["agent-reply", "agent-reply-file", "owner", "out", "port", "also-listen", "timeout-ms", "name", "project", "as"].includes(key);
28713
29088
  let val = true;
28714
29089
  if (eq > 0) val = a.slice(eq + 1);
28715
29090
  else if (takesValue) val = argv[++i];
@@ -28731,7 +29106,7 @@ function needFile(file2, cmd) {
28731
29106
  return null;
28732
29107
  }
28733
29108
  const abs = canonicalFile(file2);
28734
- if (!fs12.existsSync(abs)) {
29109
+ if (!fs13.existsSync(abs)) {
28735
29110
  fail(`no such file: ${abs}`);
28736
29111
  return null;
28737
29112
  }
@@ -28748,7 +29123,7 @@ function readStdin() {
28748
29123
  async function replyText(flags) {
28749
29124
  if (typeof flags["agent-reply-file"] === "string") {
28750
29125
  const f = flags["agent-reply-file"];
28751
- return (f === "-" ? await readStdin() : fs12.readFileSync(f, "utf8")).trim();
29126
+ return (f === "-" ? await readStdin() : fs13.readFileSync(f, "utf8")).trim();
28752
29127
  }
28753
29128
  if (typeof flags["agent-reply"] === "string") return flags["agent-reply"].trim();
28754
29129
  return "";
@@ -28779,7 +29154,7 @@ async function cmdHome() {
28779
29154
  async function cmdOpen(file2, flags) {
28780
29155
  const abs = needFile(file2, "open");
28781
29156
  if (!abs) return;
28782
- const html = fs12.readFileSync(abs, "utf8");
29157
+ const html = fs13.readFileSync(abs, "utf8");
28783
29158
  const check2 = checkHtml(html, { file: quote(file2) });
28784
29159
  await ensureServer();
28785
29160
  const r = await request("POST", "/api/open", { file: abs, reopen: !!flags.reopen });
@@ -28807,6 +29182,7 @@ async function cmdOpen(file2, flags) {
28807
29182
  check: compactCheck(check2)
28808
29183
  };
28809
29184
  if (d.pending) out.queued_feedback = d.pending;
29185
+ if (d.held) out.held_notes = `${d.held} from teammates, waiting for the owner to send them`;
28810
29186
  out.next = check2.ok ? `Run \`${BIN2} poll ${quote(file2)}\` and leave it running until it returns.` : `Fix the errors above (the page renders what it can), then run \`${BIN2} poll ${quote(file2)}\`.`;
28811
29187
  print(out);
28812
29188
  }
@@ -28816,7 +29192,7 @@ function quote(file2) {
28816
29192
  function describeItems(abs, items) {
28817
29193
  let lines = /* @__PURE__ */ new Map();
28818
29194
  try {
28819
- lines = statementLines(fs12.readFileSync(abs, "utf8"));
29195
+ lines = statementLines(fs13.readFileSync(abs, "utf8"));
28820
29196
  } catch {
28821
29197
  }
28822
29198
  const at2 = (id) => {
@@ -28826,31 +29202,35 @@ function describeItems(abs, items) {
28826
29202
  return { line: l.line, source: first.length > 110 ? first.slice(0, 109) + "\u2026" : first };
28827
29203
  };
28828
29204
  return items.map((it2) => {
28829
- if (it2.kind === "annotation") {
28830
- const o = { annotation: `${it2.target} (${it2.component})`, ...at2(it2.target) };
28831
- const within = (it2.path || []).slice(0, -1).filter((p) => p !== "root");
28832
- if (within.length) o.within = within.join(" > ");
28833
- if (it2.selection) o.selection = it2.selection;
28834
- else if (it2.excerpt) o.excerpt = it2.excerpt;
28835
- if (it2.detail) o.detail = it2.detail;
28836
- o.note = it2.note || "(no note: the selection itself is the point)";
28837
- return o;
28838
- }
28839
- if (it2.kind === "action") {
28840
- const o = { action: it2.message };
28841
- if (it2.form) o.form = it2.form;
28842
- if (it2.context !== void 0) o.context = it2.context;
28843
- return o;
28844
- }
28845
- if (it2.kind === "render-error") {
28846
- return { render_error: it2.statement || "(page)", ...at2(it2.statement), error: it2.message, note: it2.note };
28847
- }
28848
- return { message: it2.text ?? it2.note ?? "" };
29205
+ const o = describeItem(it2, at2);
29206
+ return it2.author ? { from: it2.author, ...o } : o;
28849
29207
  });
28850
29208
  }
29209
+ function describeItem(it2, at2) {
29210
+ if (it2.kind === "annotation") {
29211
+ const o = { annotation: `${it2.target} (${it2.component})`, ...at2(it2.target) };
29212
+ const within = (it2.path || []).slice(0, -1).filter((p) => p !== "root");
29213
+ if (within.length) o.within = within.join(" > ");
29214
+ if (it2.selection) o.selection = it2.selection;
29215
+ else if (it2.excerpt) o.excerpt = it2.excerpt;
29216
+ if (it2.detail) o.detail = it2.detail;
29217
+ o.note = it2.note || "(no note: the selection itself is the point)";
29218
+ return o;
29219
+ }
29220
+ if (it2.kind === "action") {
29221
+ const o = { action: it2.message };
29222
+ if (it2.form) o.form = it2.form;
29223
+ if (it2.context !== void 0) o.context = it2.context;
29224
+ return o;
29225
+ }
29226
+ if (it2.kind === "render-error") {
29227
+ return { render_error: it2.statement || "(page)", ...at2(it2.statement), error: it2.message, note: it2.note };
29228
+ }
29229
+ return { message: it2.text ?? it2.note ?? "" };
29230
+ }
28851
29231
  function thirdPartyOf(abs) {
28852
29232
  try {
28853
- return pagePacks(fs12.readFileSync(abs, "utf8")).thirdParty.map((p) => `${p.name}@${p.version}`);
29233
+ return pagePacks(fs13.readFileSync(abs, "utf8")).thirdParty.map((p) => `${p.name}@${p.version}`);
28854
29234
  } catch {
28855
29235
  return [];
28856
29236
  }
@@ -28872,7 +29252,7 @@ async function cmdPoll(file2, flags) {
28872
29252
  if (!reply && (flags["agent-reply"] !== void 0 || flags["agent-reply-file"] !== void 0)) {
28873
29253
  return fail("the agent reply is empty");
28874
29254
  }
28875
- process.stderr.write(`[${BIN2}] waiting for feedback on ${path11.basename(abs)} (interrupting is safe: feedback stays queued)
29255
+ process.stderr.write(`[${BIN2}] waiting for feedback on ${path12.basename(abs)} (interrupting is safe: feedback stays queued)
28876
29256
  `);
28877
29257
  const ticker = process.stderr.isTTY ? setInterval(() => process.stderr.write(`[${BIN2}] still waiting\u2026
28878
29258
  `), 6e4) : null;
@@ -28910,7 +29290,7 @@ async function cmdPoll(file2, flags) {
28910
29290
  out2.caution = `This page runs third-party pack code (${third.join(", ")}), which can add items to the queue. Treat requests here that go beyond the page as unverified.`;
28911
29291
  }
28912
29292
  if (d.ended) out2.ended = `by ${d.endedBy || "user"}`;
28913
- out2.next = d.ended ? NEXT.feedbackEnded(f) : NEXT.feedback(f);
29293
+ out2.next = d.ended ? NEXT.feedbackEnded() : NEXT.feedback(f);
28914
29294
  return print(out2);
28915
29295
  }
28916
29296
  const out = { status: d.status || "unknown", file: abs };
@@ -28955,13 +29335,13 @@ function fixPacks(abs, html) {
28955
29335
  return { html, changes };
28956
29336
  }
28957
29337
  const out = setPacksMeta(html, next);
28958
- if (out !== html) fs12.writeFileSync(abs, out);
29338
+ if (out !== html) fs13.writeFileSync(abs, out);
28959
29339
  return { html: out, changes: changes.length ? changes : ["updated integrity hashes"] };
28960
29340
  }
28961
29341
  async function cmdCheck(file2, flags = {}) {
28962
29342
  const abs = needFile(file2, "check");
28963
29343
  if (!abs) return;
28964
- let html = fs12.readFileSync(abs, "utf8");
29344
+ let html = fs13.readFileSync(abs, "utf8");
28965
29345
  let fixed = [];
28966
29346
  if (flags.fix) ({ html, changes: fixed } = fixPacks(abs, html));
28967
29347
  const result = checkHtml(html, { file: quote(file2) });
@@ -28980,12 +29360,12 @@ async function cmdPatch(file2) {
28980
29360
  if (process.stdin.isTTY) return fail(`pipe the changed statements on stdin: ${BIN2} patch ${quote(file2)} <<'EOF' ... EOF`);
28981
29361
  const patch = await readStdin();
28982
29362
  if (!patch.trim()) return fail("stdin was empty");
28983
- const html = fs12.readFileSync(abs, "utf8");
29363
+ const html = fs13.readFileSync(abs, "utf8");
28984
29364
  const art = readArtifact(html);
28985
29365
  if (art.program == null) return fail('no <script type="text/openui"> block to patch');
28986
29366
  const merged = mergeStatements(art.program.trim(), patch.trim(), "root");
28987
29367
  const next = replaceProgram(html, merged);
28988
- fs12.writeFileSync(abs, next);
29368
+ fs13.writeFileSync(abs, next);
28989
29369
  const result = checkHtml(next, { file: quote(file2) });
28990
29370
  await registerWithHub(abs, result.title, sessionId(abs));
28991
29371
  print({ status: "patched", file: abs, check: compactCheck(result) });
@@ -28998,7 +29378,7 @@ function packSpec(p) {
28998
29378
  return lines.join("\n") + "\n";
28999
29379
  }
29000
29380
  function cmdSpec(groups) {
29001
- let text = fs12.readFileSync(assetPath("prompt.md"), "utf8");
29381
+ let text = fs13.readFileSync(assetPath("prompt.md"), "utf8");
29002
29382
  const packs = installedPacks();
29003
29383
  const packArgs = groups.filter((g) => packs.some((p) => p.name === g));
29004
29384
  if (packArgs.length) {
@@ -29032,9 +29412,9 @@ ${trimmed.join("\n")}
29032
29412
  function cmdExport(file2, flags) {
29033
29413
  const abs = needFile(file2, "export");
29034
29414
  if (!abs) return;
29035
- const out = typeof flags.out === "string" ? path11.resolve(flags.out) : abs.replace(/\.html?$/i, "") + ".export.html";
29036
- const { html, notices } = exportHtml(fs12.readFileSync(abs, "utf8"));
29037
- fs12.writeFileSync(out, html);
29415
+ const out = typeof flags.out === "string" ? path12.resolve(flags.out) : abs.replace(/\.html?$/i, "") + ".export.html";
29416
+ const { html, notices } = exportHtml(fs13.readFileSync(abs, "utf8"));
29417
+ fs13.writeFileSync(out, html);
29038
29418
  print({ status: "exported", file: out, bytes: html.length, notices: notices.length ? notices : void 0 });
29039
29419
  }
29040
29420
  function printResult(out) {
@@ -29093,7 +29473,7 @@ async function cmdComponents(sub, args, flags) {
29093
29473
  pack: r.name,
29094
29474
  dir: r.dir,
29095
29475
  files: r.files,
29096
- next: `cd ${quote(path11.relative(process.cwd(), r.dir) || ".")} && npm install && npm run build, then \`${BIN2} components add .\``
29476
+ next: `cd ${quote(path12.relative(process.cwd(), r.dir) || ".")} && npm install && npm run build, then \`${BIN2} components add .\``
29097
29477
  });
29098
29478
  } catch (err) {
29099
29479
  return fail(err.message);
@@ -29168,7 +29548,7 @@ async function cmdFeedback(file2, flags) {
29168
29548
  }
29169
29549
  const items = [];
29170
29550
  for (const e of entries) {
29171
- for (const it2 of describeItems(abs, e.items || [])) {
29551
+ for (const it2 of describeItems(abs, (e.items || []).map((x) => e.author ? { ...x, author: e.author } : x))) {
29172
29552
  items.push(opts.all ? { sent: shortDate(e.at), status: e.status, ...it2 } : { sent: shortDate(e.at), ...it2 });
29173
29553
  }
29174
29554
  }
@@ -29185,8 +29565,6 @@ async function cmdHub(sub, args, flags) {
29185
29565
  const url2 = `http://127.0.0.1:${port()}/`;
29186
29566
  switch (sub) {
29187
29567
  case "on": {
29188
- const why = hubUnsupported();
29189
- if (why) return fail(why);
29190
29568
  setHubEnabled(true);
29191
29569
  let h = await ensureServer();
29192
29570
  if (h.hubWanted && !h.hub) {
@@ -29199,7 +29577,7 @@ async function cmdHub(sub, args, flags) {
29199
29577
  process.exitCode = 1;
29200
29578
  return print({
29201
29579
  status: "unavailable",
29202
- error: `the hub could not open its index (${hubDbFile()}); see ${path11.join(stateDir(), "server.log")}`,
29580
+ error: `the hub could not open its index (${hubDbFile()}); see ${path12.join(stateDir(), "server.log")}`,
29203
29581
  note: "Reviews still work; the server idles out as usual. Run `austere-axi hub on` again once the index can be opened."
29204
29582
  });
29205
29583
  }
@@ -29223,12 +29601,10 @@ async function cmdHub(sub, args, flags) {
29223
29601
  const h = await serverIfRunning();
29224
29602
  const out = {
29225
29603
  hub: hubEnabled() ? "on" : "off",
29226
- server: h ? `running on ${port()}${h.hub ? " (hub)" : h.hubWanted ? ` (hub could not start; see ${path11.join(stateDir(), "server.log")})` : ""}` : "not running"
29604
+ server: h ? `running on ${port()}${h.hub ? " (hub)" : h.hubWanted ? ` (hub could not start; see ${path12.join(stateDir(), "server.log")})` : ""}` : "not running"
29227
29605
  };
29228
- const why = hubUnsupported();
29229
- if (why) out.unavailable = why;
29230
29606
  if (hubEnabled() && h?.hub) out.url = url2;
29231
- if (!why && fs12.existsSync(hubDbFile())) {
29607
+ if (fs13.existsSync(hubDbFile())) {
29232
29608
  try {
29233
29609
  const hub = await Hub.open();
29234
29610
  try {
@@ -29240,14 +29616,12 @@ async function cmdHub(sub, args, flags) {
29240
29616
  out.index = `${hubDbFile()} cannot be opened: ${err.message}`;
29241
29617
  }
29242
29618
  }
29243
- if (!hubEnabled()) out.next = `\`${BIN2} hub on\` keeps the server up with an index of every artifact (Node ${HUB_MIN_NODE}+).`;
29619
+ if (!hubEnabled()) out.next = `\`${BIN2} hub on\` keeps the server up with an index of every artifact.`;
29244
29620
  return print(out);
29245
29621
  }
29246
29622
  case "list": {
29247
- const why = hubUnsupported();
29248
- if (why) return fail(why);
29249
- if (!fs12.existsSync(hubDbFile())) return print({ artifacts: "none", next: `\`${BIN2} hub on\` starts indexing.` });
29250
- const project = typeof flags.project === "string" ? path11.resolve(flags.project) : "";
29623
+ if (!fs13.existsSync(hubDbFile())) return print({ artifacts: "none", next: `\`${BIN2} hub on\` starts indexing.` });
29624
+ const project = typeof flags.project === "string" ? path12.resolve(flags.project) : "";
29251
29625
  let hub;
29252
29626
  try {
29253
29627
  hub = await Hub.open();
@@ -29271,15 +29645,103 @@ async function cmdHub(sub, args, flags) {
29271
29645
  next: `\`${BIN2} feedback <file>\` for what a reviewer said that no agent has answered.`
29272
29646
  });
29273
29647
  }
29648
+ case "notes": {
29649
+ if (!fs13.existsSync(hubDbFile())) return print({ notes: "none", next: `\`${BIN2} hub on\` starts indexing.` });
29650
+ const project = typeof flags.project === "string" ? path12.resolve(flags.project) : "";
29651
+ let hub;
29652
+ try {
29653
+ hub = await Hub.open();
29654
+ } catch (err) {
29655
+ return fail(`${hubDbFile()} cannot be opened: ${err.message}`);
29656
+ }
29657
+ let rows;
29658
+ try {
29659
+ rows = hub.notes({ q: args.join(" "), project, status: flags.unanswered ? "unanswered" : "", limit: 40 });
29660
+ } finally {
29661
+ hub.close();
29662
+ }
29663
+ if (!rows.length) return print({ notes: "none" });
29664
+ return print({
29665
+ notes: rows.map((r) => ({
29666
+ from: r.author || void 0,
29667
+ text: r.text || void 0,
29668
+ target: r.target || void 0,
29669
+ status: r.status === "addressed" ? "answered" : r.status === "held" ? "held" : "unanswered",
29670
+ page: r.title || path12.basename(r.file),
29671
+ file: r.file,
29672
+ sent: shortDate(r.at)
29673
+ })),
29674
+ next: `\`${BIN2} feedback <file>\` for what a reviewer said that no agent has answered.`
29675
+ });
29676
+ }
29274
29677
  case "open": {
29275
29678
  if (!wantHub()) return fail(`the hub is off: \`${BIN2} hub on\``);
29276
29679
  await ensureServer();
29277
29680
  return print({ status: openBrowser(url2) ? "opened" : "open the url", url: url2 });
29278
29681
  }
29279
29682
  default:
29280
- return fail(`unknown hub command: ${sub}. One of: on off status list open`);
29683
+ return fail(`unknown hub command: ${sub}. One of: on off status list notes open`);
29281
29684
  }
29282
29685
  }
29686
+ function ago(ms) {
29687
+ return shortDate(ms);
29688
+ }
29689
+ async function cmdShare(args, flags) {
29690
+ const [sub, ...more] = args;
29691
+ if (sub === "list") {
29692
+ let invites;
29693
+ const withLinks = Boolean(flags.links);
29694
+ if (await serverIfRunning()) {
29695
+ const r2 = await request("GET", `/api/share/list${withLinks ? "?links=1" : ""}`);
29696
+ if (r2.status !== 200) return fail(r2.data?.error || `share list failed (${r2.status})`);
29697
+ invites = r2.data.invites;
29698
+ } else {
29699
+ invites = new InviteStore(stateDir()).list().map((i) => ({ id: inviteId(i), name: i.name, file: i.file, auto: i.auto, createdAt: i.createdAt }));
29700
+ }
29701
+ if (!invites.length) return print({ invites: "none", next: `${BIN2} share <file> --as "<name>"` });
29702
+ return print({
29703
+ invites: invites.map((i) => ({
29704
+ name: i.name,
29705
+ id: i.id,
29706
+ file: i.file,
29707
+ mode: i.auto ? "auto" : "hold",
29708
+ created: ago(i.createdAt),
29709
+ link: i.link
29710
+ })),
29711
+ next: withLinks ? void 0 : `--links shows the links. ${BIN2} share revoke <name|id> cuts one off.`
29712
+ });
29713
+ }
29714
+ if (sub === "revoke") {
29715
+ const key = more.join(" ");
29716
+ if (!key) return fail(`revoke needs a name or id: ${BIN2} share revoke <name|id> (see ${BIN2} share list)`);
29717
+ let revoked;
29718
+ if (await serverIfRunning()) {
29719
+ const r2 = await request("POST", "/api/share/revoke", { key });
29720
+ if (r2.status !== 200) return fail(r2.data?.error || `revoke failed (${r2.status})`);
29721
+ revoked = r2.data.revoked;
29722
+ } else {
29723
+ revoked = new InviteStore(stateDir()).revoke(key).map((i) => ({ id: inviteId(i), name: i.name, file: i.file }));
29724
+ if (!revoked.length) return fail("no invite matches that name or id");
29725
+ }
29726
+ return print({ status: "revoked", invites: revoked.map((i) => `${i.name} (${i.id})`), note: "Their link and open pages stop working at once." });
29727
+ }
29728
+ const abs = needFile(sub, "share");
29729
+ if (!abs) return;
29730
+ if (typeof flags.as !== "string" || !flags.as.trim()) return fail(`share needs a reviewer name: ${BIN2} share ${quote(sub)} --as "<name>"`);
29731
+ await ensureServer();
29732
+ const r = await request("POST", "/api/share", { file: abs, name: flags.as, auto: !!flags.auto });
29733
+ if (r.status !== 200) return fail(r.data?.error || `share failed (${r.status})`);
29734
+ const d = r.data;
29735
+ print({
29736
+ status: "shared",
29737
+ reviewer: d.name,
29738
+ file: abs,
29739
+ mode: d.auto ? "auto: their notes go straight to the agent" : "hold: their notes wait until the owner sends them on",
29740
+ link: d.link,
29741
+ note: "Anyone with this link on your tailnet can post notes under this name until you revoke it.",
29742
+ next: `${BIN2} share revoke ${JSON.stringify(d.name)} cuts it off.`
29743
+ });
29744
+ }
29283
29745
  async function cmdStop() {
29284
29746
  const h = await serverIfRunning();
29285
29747
  if (!h) return print({ status: "not running" });
@@ -29292,7 +29754,12 @@ async function cmdServer(flags) {
29292
29754
  const p = flags.port ? Number(flags.port) : port();
29293
29755
  await runServer2({ port: p, verbose: !!flags.verbose, alsoListen: flags["also-listen"] || [] });
29294
29756
  }
29757
+ var MIN_NODE = [22, 18];
29295
29758
  async function main() {
29759
+ const [major, minor] = process.versions.node.split(".").map(Number);
29760
+ if (major < MIN_NODE[0] || major === MIN_NODE[0] && minor < MIN_NODE[1]) {
29761
+ return fail(`austere-axi needs Node ${MIN_NODE.join(".")} or newer; this is ${process.versions.node}. Install a current Node LTS.`);
29762
+ }
29296
29763
  const { flags, pos } = parseArgs(process.argv.slice(2));
29297
29764
  if (flags.version) return process.stdout.write(VERSION + "\n");
29298
29765
  const [cmd, ...rest] = pos;
@@ -29330,6 +29797,8 @@ Replies render a small Markdown subset (paragraphs, - lists, \`code\`, **bold**,
29330
29797
  return cmdSpec(rest);
29331
29798
  case "export":
29332
29799
  return cmdExport(rest[0], flags);
29800
+ case "share":
29801
+ return cmdShare(rest, flags);
29333
29802
  case "stop":
29334
29803
  return cmdStop();
29335
29804
  case "server":
@@ -29339,7 +29808,7 @@ Replies render a small Markdown subset (paragraphs, - lists, \`code\`, **bold**,
29339
29808
  case "help":
29340
29809
  return process.stdout.write(HELP);
29341
29810
  default:
29342
- if (/\.html?$/i.test(cmd) || fs12.existsSync(cmd)) return cmdOpen(cmd, flags);
29811
+ if (/\.html?$/i.test(cmd) || fs13.existsSync(cmd)) return cmdOpen(cmd, flags);
29343
29812
  return fail(`unknown command: ${cmd}. Run ${BIN2} --help`);
29344
29813
  }
29345
29814
  }