austere-axi 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1" : 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 {
@@ -1751,6 +1916,19 @@ function allowedHosts(extra = []) {
1751
1916
  for (const h of String(process.env.AUSTERE_AXI_ALLOWED_HOSTS || "").split(/\s+/)) if (h) set2.add(h.toLowerCase());
1752
1917
  return set2;
1753
1918
  }
1919
+ function linkUrl() {
1920
+ const raw = String(process.env.AUSTERE_AXI_LINK_URL || "").trim();
1921
+ if (!raw) return null;
1922
+ let u;
1923
+ try {
1924
+ u = new URL(raw);
1925
+ } catch {
1926
+ throw new Error(`AUSTERE_AXI_LINK_URL is not a URL: ${raw}`);
1927
+ }
1928
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`AUSTERE_AXI_LINK_URL must be http or https: ${raw}`);
1929
+ if (u.pathname !== "/" || u.search || u.hash || u.username || u.password) throw new Error(`AUSTERE_AXI_LINK_URL must be just scheme, host and optional port, like https://austere.example.com: ${raw}`);
1930
+ return { base: u.origin, host: u.hostname.toLowerCase() };
1931
+ }
1754
1932
  function hostOnly(hostHeader) {
1755
1933
  const h = String(hostHeader || "").toLowerCase();
1756
1934
  if (h.startsWith("[")) return h.slice(0, h.indexOf("]") + 1);
@@ -1775,7 +1953,7 @@ var init_net = __esm({
1775
1953
 
1776
1954
  // src/cli/hub-page.js
1777
1955
  import os2 from "node:os";
1778
- import path9 from "node:path";
1956
+ import path10 from "node:path";
1779
1957
  function day(ms) {
1780
1958
  const d = new Date(ms);
1781
1959
  const today = /* @__PURE__ */ new Date();
@@ -1790,7 +1968,7 @@ function time3(ms) {
1790
1968
  }
1791
1969
  function tilde(p) {
1792
1970
  const home = os2.homedir();
1793
- return p === home || p.startsWith(home + path9.sep) ? "~" + p.slice(home.length) : p;
1971
+ return p === home || p.startsWith(home + path10.sep) ? "~" + p.slice(home.length) : p;
1794
1972
  }
1795
1973
  function counts(a) {
1796
1974
  const waiting = a.open + a.received;
@@ -1824,13 +2002,13 @@ function hubPage(rows, { version: version2, q: q2, project, stats }) {
1824
2002
  const body = [...days.entries()].map(
1825
2003
  ([d, items]) => `<h3>${escapeHtml(d)}</h3><ul>${items.map(
1826
2004
  (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>
2005
+ <strong>${escapeHtml(a.title || path10.basename(a.file))}</strong>
2006
+ <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
2007
  </a></li>`
1830
2008
  ).join("")}</ul>`
1831
2009
  ).join("");
1832
2010
  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(
2011
+ return `<section><h2><a href="/?project=${encodeURIComponent(proj)}">${escapeHtml(path10.basename(proj) || proj)}</a> <small title="${escapeHtml(proj)}">${escapeHtml(
1834
2012
  shortPath(proj)
1835
2013
  )}</small>${unanswered ? ` <b class="open">${unanswered} unanswered</b>` : ""}</h2>${body}</section>`;
1836
2014
  }).join("");
@@ -1839,7 +2017,42 @@ function hubPage(rows, { version: version2, q: q2, project, stats }) {
1839
2017
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
1840
2018
  <title>austere-axi hub</title><meta name="color-scheme" content="dark">
1841
2019
  <style>
1842
- body{margin:0;background:#0b0b0d;color:#e8e8ec;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
2020
+ ${STYLE}
2021
+ </style></head>
2022
+ <body><main>
2023
+ <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>
2024
+ <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>
2025
+ ${filter}${sections || `<p class="empty">${empty}</p>`}
2026
+ </main></body></html>`;
2027
+ }
2028
+ function notesPage(rows, { version: version2, q: q2, project, status, author = "", authors = [], stats }) {
2029
+ const items = rows.map((n) => {
2030
+ const waiting = n.status !== "addressed";
2031
+ const when = `${day(n.at)} ${time3(n.at)}`;
2032
+ return `<li class="n"><a href="/hub/open/${escapeHtml(n.artifact_id)}" title="${escapeHtml(n.file)}">
2033
+ <p class="note">${escapeHtml(n.text || "(no text)")}</p>
2034
+ <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>
2035
+ </a></li>`;
2036
+ }).join("");
2037
+ const opt = (v, label) => `<option value="${v}"${status === v ? " selected" : ""}>${label}</option>`;
2038
+ 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>` : "";
2039
+ const filter = project ? `<p class="filter">Project ${escapeHtml(tilde(project))} \xB7 <a href="/notes">all projects</a></p>` : "";
2040
+ const empty = q2 || project || author || status !== "all" ? "No notes match." : "No notes yet.";
2041
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
2042
+ <title>Notes \xB7 austere-axi hub</title><meta name="color-scheme" content="dark">
2043
+ <style>
2044
+ ${STYLE}</style></head>
2045
+ <body><main>
2046
+ <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>
2047
+ <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>
2048
+ ${filter}${items ? `<ul>${items}</ul>` : `<p class="empty">${empty}</p>`}
2049
+ </main></body></html>`;
2050
+ }
2051
+ var STYLE, STATUS_LABEL;
2052
+ var init_hub_page = __esm({
2053
+ "src/cli/hub-page.js"() {
2054
+ init_artifact();
2055
+ STYLE = `body{margin:0;background:#0b0b0d;color:#e8e8ec;font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}
1843
2056
  main{max-width:860px;margin:0 auto;padding:32px 16px 64px}
1844
2057
  header{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;justify-content:space-between}
1845
2058
  h1{font-size:18px;font-weight:600;margin:0}h1 small{color:#77777f;font-weight:400;font-size:13px;margin-left:6px}
@@ -1855,16 +2068,14 @@ li strong{color:#a9c1ff;font-weight:600;overflow-wrap:anywhere}li>a:hover strong
1855
2068
  .meta{color:#8a8a94;font-size:12px;overflow-wrap:anywhere}em{font-style:normal}em.live{color:#8fd19e}
1856
2069
  b{font-weight:600}b.open{color:#f2c46d;font-size:12px}
1857
2070
  .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();
2071
+ .nav{display:flex;gap:14px;font-size:13px}.nav a{color:#a9c1ff;text-decoration:none}
2072
+ 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}
2073
+ .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}
2074
+ li.n{padding:12px 0}li.n>a{padding:0}
2075
+ b.by{color:#e8e8ec}
2076
+ @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}}
2077
+ `;
2078
+ STATUS_LABEL = { open: "unanswered", received: "unanswered", addressed: "answered", held: "held for the owner" };
1868
2079
  }
1869
2080
  });
1870
2081
 
@@ -1873,30 +2084,26 @@ var server_exports = {};
1873
2084
  __export(server_exports, {
1874
2085
  runServer: () => runServer
1875
2086
  });
1876
- import fs11 from "node:fs";
2087
+ import fs12 from "node:fs";
1877
2088
  import http2 from "node:http";
1878
- import path10 from "node:path";
1879
- import crypto3 from "node:crypto";
2089
+ import path11 from "node:path";
2090
+ import crypto4 from "node:crypto";
1880
2091
  async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
1881
2092
  const log = (...a) => {
1882
2093
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${a.join(" ")}`;
1883
2094
  if (verbose) process.stderr.write(line + "\n");
1884
2095
  try {
1885
- fs11.appendFileSync(path10.join(stateDir(), "server.log"), line + "\n");
2096
+ fs12.appendFileSync(path11.join(stateDir(), "server.log"), line + "\n");
1886
2097
  } catch {
1887
2098
  }
1888
2099
  };
1889
2100
  let hub = null;
1890
2101
  const hubWanted = hubEnabled();
1891
2102
  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
- }
2103
+ try {
2104
+ hub = await Hub.open();
2105
+ } catch (err) {
2106
+ log("hub-unavailable", String(err?.message || err));
1900
2107
  }
1901
2108
  }
1902
2109
  const store = new SessionStore(stateDir(), { onFlush: hub ? (s) => hub.syncSession(s) : void 0 });
@@ -1909,6 +2116,8 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
1909
2116
  }
1910
2117
  }
1911
2118
  }
2119
+ const invites = new InviteStore(stateDir());
2120
+ const ownerToken = ownerKey(stateDir());
1912
2121
  const clients = /* @__PURE__ */ new Map();
1913
2122
  const polls = /* @__PURE__ */ new Map();
1914
2123
  const watchers = /* @__PURE__ */ new Map();
@@ -1917,6 +2126,7 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
1917
2126
  const byeAt = /* @__PURE__ */ new Map();
1918
2127
  const browserSeen = /* @__PURE__ */ new Set();
1919
2128
  let tailnet = null;
2129
+ const linkBase = linkUrl();
1920
2130
  let lastActivity = Date.now();
1921
2131
  const longGraceMs = msEnv("AUSTERE_AXI_DISCONNECT_GRACE_MS", 5 * 6e4);
1922
2132
  const idleMs = hub ? 0 : msEnv("AUSTERE_AXI_IDLE_TIMEOUT_MS", 30 * 6e4);
@@ -1926,17 +2136,53 @@ async function runServer({ port: port2, verbose = false, alsoListen = [] }) {
1926
2136
  function hostsAllowed() {
1927
2137
  const extra = [];
1928
2138
  if (tailnet) extra.push(tailnet.ip, tailnet.dnsName);
2139
+ if (linkBase) extra.push(linkBase.host);
1929
2140
  for (const h of listeners.keys()) extra.push(h);
1930
2141
  return allowedHosts(extra);
1931
2142
  }
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}`;
2143
+ function remoteHost() {
1936
2144
  const linkHost = process.env.AUSTERE_AXI_LINK_HOST;
1937
- if (linkHost) out.remote = `http://${linkHost}:${port2}/s/${id}`;
2145
+ if (linkHost) return linkHost;
2146
+ if (tailnet && listeners.has(tailnet.ip)) return tailnet.dnsName || tailnet.ip;
2147
+ const explicit = process.env.AUSTERE_AXI_HOST;
2148
+ if (explicit && !/^(0\.0\.0\.0|::|\*)$/.test(explicit) && listeners.has(explicit)) return explicit;
2149
+ return null;
2150
+ }
2151
+ function remoteBase() {
2152
+ if (linkBase) return linkBase.base;
2153
+ const host = remoteHost();
2154
+ return host ? `http://${host}:${port2}` : null;
2155
+ }
2156
+ function remoteUrl(id) {
2157
+ const base = remoteBase();
2158
+ return base ? `${base}/s/${id}` : null;
2159
+ }
2160
+ function links(id) {
2161
+ const out = { local: `http://127.0.0.1:${port2}/s/${id}` };
2162
+ const remote = remoteUrl(id);
2163
+ if (remote) out.remote = `${remote}?key=${ownerToken}`;
1938
2164
  return out;
1939
2165
  }
2166
+ function inviteLink(inv) {
2167
+ return `${remoteUrl(inv.sessionId)}?invite=${inv.token}`;
2168
+ }
2169
+ function isOwner(req) {
2170
+ if (isLoopback(req.socket.remoteAddress)) return true;
2171
+ const key = tokenFromCookie(req.headers.cookie, OWNER_COOKIE);
2172
+ return key != null && safeEqual(key, ownerToken);
2173
+ }
2174
+ function identify(req, s) {
2175
+ const token = tokenFromCookie(req.headers.cookie);
2176
+ if (token == null) return isOwner(req) ? OWNER : null;
2177
+ const inv = invites.find(token);
2178
+ if (!inv || inv.sessionId !== s.id) return null;
2179
+ return { role: "reviewer", name: inv.name, auto: inv.auto, token: inv.token };
2180
+ }
2181
+ function canSee(e, viewer) {
2182
+ if (viewer.role === "owner") return true;
2183
+ if (e.role === "user" && (e.held || e.discarded)) return e.author === viewer.name;
2184
+ return true;
2185
+ }
1940
2186
  function send(res, status, body, headers = {}) {
1941
2187
  const isText = typeof body === "string" || Buffer.isBuffer(body);
1942
2188
  const payload = isText ? body : JSON.stringify(body);
@@ -1985,11 +2231,32 @@ data: ${JSON.stringify(data)}
1985
2231
  broadcast(s.id, "presence", { presence: presence(s), status: s.status, endedBy: s.endedBy });
1986
2232
  }
1987
2233
  function broadcastTranscript(s, entries) {
1988
- broadcast(s.id, "transcript", { entries });
2234
+ const set2 = clients.get(s.id);
2235
+ if (!set2) return;
2236
+ for (const c of set2) {
2237
+ const visible = entries.filter((e) => canSee(e, c.viewer));
2238
+ if (visible.length) c.res.write(`event: transcript
2239
+ data: ${JSON.stringify({ entries: visible })}
2240
+
2241
+ `);
2242
+ }
2243
+ }
2244
+ function dropReviewers(tokens) {
2245
+ for (const set2 of clients.values()) {
2246
+ for (const c of Array.from(set2)) {
2247
+ if (c.viewer.role !== "reviewer" || !tokens.has(c.viewer.token)) continue;
2248
+ c.res.write(`event: server
2249
+ data: ${JSON.stringify({ state: "revoked" })}
2250
+
2251
+ `);
2252
+ c.res.end();
2253
+ set2.delete(c);
2254
+ }
2255
+ }
1989
2256
  }
1990
2257
  function readFileSafe(file2) {
1991
2258
  try {
1992
- return fs11.readFileSync(file2, "utf8");
2259
+ return fs12.readFileSync(file2, "utf8");
1993
2260
  } catch {
1994
2261
  return null;
1995
2262
  }
@@ -2001,14 +2268,14 @@ data: ${JSON.stringify(data)}
2001
2268
  const w = { html, program: art?.program ?? null, shell: art?.shell ?? null, timer: null };
2002
2269
  watchers.set(s.id, w);
2003
2270
  if (art?.title) s.title = art.title;
2004
- fs11.watchFile(s.file, { interval: 300 }, () => {
2271
+ fs12.watchFile(s.file, { interval: 300 }, () => {
2005
2272
  clearTimeout(w.timer);
2006
2273
  w.timer = setTimeout(() => onFileChange(s, w), 120);
2007
2274
  });
2008
2275
  }
2009
2276
  function unwatch(id) {
2010
2277
  const s = store.get(id);
2011
- if (s) fs11.unwatchFile(s.file);
2278
+ if (s) fs12.unwatchFile(s.file);
2012
2279
  watchers.delete(id);
2013
2280
  }
2014
2281
  function onFileChange(s, w) {
@@ -2050,7 +2317,7 @@ data: ${JSON.stringify(data)}
2050
2317
  const items = [];
2051
2318
  let end = false;
2052
2319
  for (const e of entries) {
2053
- for (const it2 of e.items || []) items.push(it2);
2320
+ for (const it2 of e.items || []) items.push(e.author ? { ...it2, author: e.author } : it2);
2054
2321
  if (e.end) end = true;
2055
2322
  }
2056
2323
  const body = {
@@ -2111,6 +2378,7 @@ data: ${JSON.stringify(data)}
2111
2378
  stateDir: stateDir(),
2112
2379
  listen: Array.from(listeners.keys()),
2113
2380
  tailnet: tailnet || void 0,
2381
+ remote: remoteBase() || void 0,
2114
2382
  hub: Boolean(hub),
2115
2383
  hubWanted
2116
2384
  });
@@ -2122,6 +2390,7 @@ data: ${JSON.stringify(data)}
2122
2390
  presence: presence(s2),
2123
2391
  browsers: clientCount(s2.id),
2124
2392
  pending: s2.pending.length,
2393
+ held: store.heldEntries(s2).length,
2125
2394
  links: links(s2.id)
2126
2395
  }))
2127
2396
  });
@@ -2131,12 +2400,52 @@ data: ${JSON.stringify(data)}
2131
2400
  setTimeout(() => shutdown("requested"), 50);
2132
2401
  return;
2133
2402
  }
2403
+ if (route === "share/list") {
2404
+ const withLinks = url2.searchParams.get("links") === "1";
2405
+ return send(res, 200, {
2406
+ invites: invites.list().map((i) => ({
2407
+ id: inviteId(i),
2408
+ name: i.name,
2409
+ file: i.file,
2410
+ auto: i.auto,
2411
+ createdAt: i.createdAt,
2412
+ link: withLinks ? inviteLink(i) : void 0
2413
+ }))
2414
+ });
2415
+ }
2134
2416
  if (req.method !== "POST") return send(res, 405, { error: "POST expected" });
2135
2417
  const body = await readBody(req);
2418
+ if (route === "share/revoke") {
2419
+ const gone = invites.revoke(String(body.key || ""));
2420
+ if (!gone.length) return send(res, 404, { error: "no invite matches that name or id" });
2421
+ dropReviewers(new Set(gone.map((i) => i.token)));
2422
+ log("share-revoke", gone.map((i) => `${i.name}(${inviteId(i)})`).join(","));
2423
+ return send(res, 200, { revoked: gone.map((i) => ({ id: inviteId(i), name: i.name, file: i.file })) });
2424
+ }
2136
2425
  const file2 = typeof body.file === "string" ? body.file : "";
2137
2426
  if (!file2) return send(res, 400, { error: "file is required" });
2427
+ if (route === "share") {
2428
+ if (!fs12.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
2429
+ await ensureTailnetListener();
2430
+ if (!remoteBase()) {
2431
+ return send(res, 409, {
2432
+ error: "no tailnet address: Tailscale is not running here and neither AUSTERE_AXI_HOST nor AUSTERE_AXI_LINK_URL is set, so a teammate could not open the link"
2433
+ });
2434
+ }
2435
+ let inv;
2436
+ try {
2437
+ const s2 = store.ensure(file2);
2438
+ inv = invites.add({ name: body.name, sessionId: s2.id, file: s2.file, auto: !!body.auto });
2439
+ watch(s2);
2440
+ store.touch(s2);
2441
+ } catch (err) {
2442
+ return send(res, 400, { error: String(err.message || err) });
2443
+ }
2444
+ log("share", inv.name, inviteId(inv), inv.auto ? "auto" : "hold");
2445
+ return send(res, 200, { id: inviteId(inv), name: inv.name, auto: inv.auto, link: inviteLink(inv), file: inv.file });
2446
+ }
2138
2447
  if (route === "open") {
2139
- if (!fs11.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
2448
+ if (!fs12.existsSync(file2)) return send(res, 404, { error: `no such file: ${file2}` });
2140
2449
  await ensureTailnetListener();
2141
2450
  const s2 = store.ensure(file2);
2142
2451
  if (s2.status === "ended") {
@@ -2153,7 +2462,8 @@ data: ${JSON.stringify(data)}
2153
2462
  session: publicSession(s2),
2154
2463
  links: links(s2.id),
2155
2464
  browsers: clientCount(s2.id),
2156
- pending: s2.pending.length
2465
+ pending: s2.pending.length,
2466
+ held: store.heldEntries(s2).length
2157
2467
  });
2158
2468
  }
2159
2469
  const s = store.byFile(file2);
@@ -2246,9 +2556,9 @@ data: ${JSON.stringify(data)}
2246
2556
  }
2247
2557
  const file2 = installedPackFile(spec.name, spec.version);
2248
2558
  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) => {
2559
+ fs12.readFile(file2, (err, buf) => {
2250
2560
  if (err) return send(res, 404, "not found");
2251
- const actual = `sha384-${crypto3.createHash("sha384").update(buf).digest("base64")}`;
2561
+ const actual = `sha384-${crypto4.createHash("sha384").update(buf).digest("base64")}`;
2252
2562
  const locked = readLock().packs[spec.name]?.integrity;
2253
2563
  const wanted = url2.searchParams.get("integrity");
2254
2564
  if (locked && locked !== actual) {
@@ -2270,23 +2580,23 @@ data: ${JSON.stringify(data)}
2270
2580
  }
2271
2581
  function serveAsset(res, rel, url2) {
2272
2582
  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) => {
2583
+ const clean = path11.normalize(rel).replace(/^(\.\.[/\\])+/, "");
2584
+ const file2 = path11.join(ASSET_DIR, clean);
2585
+ if (!file2.startsWith(ASSET_DIR + path11.sep)) return send(res, 404, "not found");
2586
+ fs12.stat(file2, (err, st2) => {
2277
2587
  if (err || !st2.isFile()) return send(res, 404, "not found");
2278
2588
  res.writeHead(200, {
2279
- "content-type": MIME[path10.extname(file2)] || "application/octet-stream",
2589
+ "content-type": MIME[path11.extname(file2)] || "application/octet-stream",
2280
2590
  "content-length": st2.size,
2281
2591
  "cache-control": "no-cache",
2282
2592
  "x-content-type-options": "nosniff"
2283
2593
  });
2284
- fs11.createReadStream(file2).pipe(res);
2594
+ fs12.createReadStream(file2).pipe(res);
2285
2595
  });
2286
2596
  }
2287
2597
  function serveArtifact(req, res, s, rest) {
2288
- const dir = path10.dirname(s.file);
2289
- const base = path10.basename(s.file);
2598
+ const dir = path11.dirname(s.file);
2599
+ const base = path11.basename(s.file);
2290
2600
  if (rest === base || rest === "") {
2291
2601
  const html = readFileSafe(s.file);
2292
2602
  if (html == null) return send(res, 404, "artifact file is missing");
@@ -2299,28 +2609,29 @@ data: ${JSON.stringify(data)}
2299
2609
  } catch {
2300
2610
  return send(res, 400, "bad path");
2301
2611
  }
2302
- const file2 = path10.resolve(dir, decoded);
2303
- if (!file2.startsWith(dir + path10.sep) || decoded.split(/[/\\]/).some((p) => p.startsWith("."))) {
2612
+ const file2 = path11.resolve(dir, decoded);
2613
+ if (!file2.startsWith(dir + path11.sep) || decoded.split(/[/\\]/).some((p) => p.startsWith("."))) {
2304
2614
  return send(res, 404, "not found");
2305
2615
  }
2306
- fs11.stat(file2, (err, st2) => {
2616
+ fs12.stat(file2, (err, st2) => {
2307
2617
  if (err || !st2.isFile() || st2.size > 64 * 1024 * 1024) return send(res, 404, "not found");
2308
2618
  res.writeHead(200, {
2309
- "content-type": MIME[path10.extname(file2).toLowerCase()] || "application/octet-stream",
2619
+ "content-type": MIME[path11.extname(file2).toLowerCase()] || "application/octet-stream",
2310
2620
  "content-length": st2.size,
2311
2621
  "cache-control": "no-cache",
2312
2622
  "x-content-type-options": "nosniff"
2313
2623
  });
2314
- fs11.createReadStream(file2).pipe(res);
2624
+ fs12.createReadStream(file2).pipe(res);
2315
2625
  });
2316
2626
  }
2317
- function chromePage(s) {
2627
+ function chromePage(s, viewer) {
2318
2628
  const boot = {
2319
2629
  id: s.id,
2320
2630
  title: s.title,
2321
- file: s.file,
2322
- name: path10.basename(s.file),
2323
- artifactUrl: `/s/${s.id}/a/${encodeURIComponent(path10.basename(s.file))}`,
2631
+ file: viewer.role === "owner" ? s.file : "",
2632
+ viewer: { role: viewer.role, name: viewer.name, auto: viewer.role === "reviewer" ? viewer.auto : void 0 },
2633
+ name: path11.basename(s.file),
2634
+ artifactUrl: `/s/${s.id}/a/${encodeURIComponent(path11.basename(s.file))}`,
2324
2635
  version: VERSION
2325
2636
  };
2326
2637
  return `<!doctype html>
@@ -2343,10 +2654,20 @@ data: ${JSON.stringify(data)}
2343
2654
  const project = (url2.searchParams.get("project") || "").slice(0, 1e3);
2344
2655
  return hubPage(hub.list({ q: q2, project }), { version: VERSION, q: q2, project, stats: hub.stats() });
2345
2656
  }
2657
+ function hubNotes(url2) {
2658
+ store.flushAll();
2659
+ const q2 = (url2.searchParams.get("q") || "").slice(0, 200);
2660
+ const project = (url2.searchParams.get("project") || "").slice(0, 1e3);
2661
+ const st2 = url2.searchParams.get("status");
2662
+ const status = st2 === "unanswered" || st2 === "answered" ? st2 : "all";
2663
+ const author = (url2.searchParams.get("author") || "").slice(0, 80);
2664
+ const rows = hub.notes({ q: q2, project, author, status: status === "all" ? "" : status });
2665
+ return notesPage(rows, { version: VERSION, q: q2, project, status, author, authors: hub.authors(), stats: hub.stats() });
2666
+ }
2346
2667
  function hubOpen(res, id) {
2347
2668
  const a = /^[a-f0-9]{12}$/.test(id) ? hub.artifact(id) : null;
2348
2669
  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`);
2670
+ if (!fs12.existsSync(a.file)) return send(res, 410, `${a.file} no longer exists`);
2350
2671
  const existed = store.byFile(a.file);
2351
2672
  const s = store.ensure(a.file);
2352
2673
  if (!existed) store.touch(s);
@@ -2369,9 +2690,23 @@ data: ${JSON.stringify(data)}
2369
2690
  }
2370
2691
  if (url2.pathname.startsWith("/api/")) return handleApi(req, res, url2);
2371
2692
  if (url2.pathname.startsWith("/__austere/")) return serveAsset(res, url2.pathname.slice("/__austere/".length), url2);
2693
+ const key = url2.searchParams.get("key");
2694
+ if (key != null) {
2695
+ if (!safeEqual(key, ownerToken)) return send(res, 403, "This link is not valid.");
2696
+ url2.searchParams.delete("key");
2697
+ res.writeHead(302, {
2698
+ location: url2.pathname + url2.search,
2699
+ "set-cookie": `${OWNER_COOKIE}=${ownerToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`,
2700
+ "cache-control": "no-store"
2701
+ });
2702
+ return res.end();
2703
+ }
2704
+ const ownerOnly = url2.pathname === "/" || url2.pathname === "/notes" || url2.pathname.startsWith("/hub/");
2705
+ if (ownerOnly && !isOwner(req)) return send(res, 403, "Only the owner can open this. Ask the owner for a link to the page.");
2372
2706
  if (url2.pathname === "/") {
2373
2707
  return send(res, 200, hub ? hubHome(url2) : homePage(), { "content-type": "text/html; charset=utf-8" });
2374
2708
  }
2709
+ if (hub && url2.pathname === "/notes") return send(res, 200, hubNotes(url2), { "content-type": "text/html; charset=utf-8" });
2375
2710
  if (hub && url2.pathname.startsWith("/hub/open/")) return hubOpen(res, url2.pathname.slice("/hub/open/".length));
2376
2711
  if (url2.pathname === "/favicon.ico") return send(res, 204, "");
2377
2712
  const m = /^\/s\/([a-f0-9]{12})(\/.*)?$/.exec(url2.pathname);
@@ -2380,9 +2715,31 @@ data: ${JSON.stringify(data)}
2380
2715
  if (!s) return send(res, 404, "no such session");
2381
2716
  const sub = m[2] || "";
2382
2717
  activity();
2718
+ const invite = url2.searchParams.get("invite");
2719
+ if (invite != null && (sub === "" || sub === "/")) {
2720
+ const inv = invites.find(invite);
2721
+ 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.");
2722
+ res.writeHead(302, {
2723
+ location: `/s/${s.id}`,
2724
+ "set-cookie": `${COOKIE}=${inv.token}; Path=/s/${s.id}; HttpOnly; SameSite=Lax; Max-Age=2592000`,
2725
+ "cache-control": "no-store"
2726
+ });
2727
+ return res.end();
2728
+ }
2729
+ const who = identify(req, s);
2730
+ if (!who) {
2731
+ if (tokenFromCookie(req.headers.cookie) == null) return send(res, 403, "Ask the owner for a link to this page.");
2732
+ return send(res, 403, "This invite is no longer valid. Ask for a new link.", {
2733
+ "set-cookie": `${COOKIE}=; Path=/s/${s.id}; HttpOnly; SameSite=Lax; Max-Age=0`
2734
+ });
2735
+ }
2736
+ const reviewer = who.role === "reviewer";
2737
+ if (reviewer && !(sub === "" || sub === "/" || sub.startsWith("/a/") || sub === "/events" || sub === "/send" || sub === "/bye")) {
2738
+ return send(res, 403, { error: "reviewers can only read the page and add notes" });
2739
+ }
2383
2740
  if (sub === "" || sub === "/") {
2384
2741
  watch(s);
2385
- return send(res, 200, chromePage(s), { "content-type": "text/html; charset=utf-8" });
2742
+ return send(res, 200, chromePage(s, who), { "content-type": "text/html; charset=utf-8" });
2386
2743
  }
2387
2744
  if (sub.startsWith("/a/")) return serveArtifact(req, res, s, sub.slice(3));
2388
2745
  if (sub === "/events") {
@@ -2396,20 +2753,20 @@ data: ${JSON.stringify(data)}
2396
2753
  res.write("retry: 2000\n\n");
2397
2754
  const w = watchers.get(s.id);
2398
2755
  const snapshot = {
2399
- session: publicSession(s),
2756
+ session: reviewer ? { ...publicSession(s), file: "" } : publicSession(s),
2400
2757
  presence: presence(s),
2401
- transcript: s.transcript.slice(-200),
2758
+ transcript: s.transcript.filter((e) => !e.discarded && canSee(e, who)).slice(-200),
2402
2759
  revision: s.revision,
2403
2760
  program: w?.program ?? null,
2404
2761
  thirdPartyPacks: thirdPartyPacks(w?.html),
2405
2762
  version: VERSION,
2406
- links: links(s.id)
2763
+ links: reviewer ? null : links(s.id)
2407
2764
  };
2408
2765
  res.write(`event: snapshot
2409
2766
  data: ${JSON.stringify(snapshot)}
2410
2767
 
2411
2768
  `);
2412
- const client = { res };
2769
+ const client = { res, viewer: who };
2413
2770
  if (!clients.has(s.id)) clients.set(s.id, /* @__PURE__ */ new Set());
2414
2771
  clients.get(s.id).add(client);
2415
2772
  browserSeen.add(s.id);
@@ -2426,13 +2783,19 @@ data: ${JSON.stringify(snapshot)}
2426
2783
  if (req.method === "POST" && !checkOrigin(req)) return send(res, 403, { error: "cross-origin request refused" });
2427
2784
  if (sub === "/send" && req.method === "POST") {
2428
2785
  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) : [];
2786
+ const end = !reviewer && !!body.end;
2787
+ let items = Array.isArray(body.items) ? body.items.filter((x) => x && typeof x === "object").slice(0, reviewer ? 50 : 200) : [];
2788
+ if (reviewer) items = items.filter((x) => REVIEWER_KINDS.has(x.kind));
2430
2789
  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");
2790
+ if (!items.length && !end) return send(res, 400, { error: "nothing to send" });
2791
+ for (const it2 of items) {
2792
+ delete it2.author;
2793
+ it2.id = reviewer || !it2.id ? newId("i") : it2.id;
2794
+ }
2795
+ const hold = reviewer && !who.auto;
2433
2796
  let entry = null;
2434
- if (items.length) entry = store.addUserBatch(s, items, { end: !!body.end });
2435
- if (body.end) {
2797
+ if (items.length) entry = store.addUserBatch(s, items, { end, author: who.name, hold });
2798
+ if (end) {
2436
2799
  store.end(s, "user");
2437
2800
  broadcast(s.id, "ended", { by: "user" });
2438
2801
  }
@@ -2440,11 +2803,21 @@ data: ${JSON.stringify(snapshot)}
2440
2803
  const poll = polls.get(s.id);
2441
2804
  if (poll && !poll.done) {
2442
2805
  if (s.pending.length) deliver(s, poll);
2443
- else if (body.end) finishPoll(s, poll, { status: "ended", endedBy: "user" });
2806
+ else if (end) finishPoll(s, poll, { status: "ended", endedBy: "user" });
2444
2807
  }
2445
2808
  broadcastPresence(s);
2446
2809
  return send(res, 200, { ok: true, entry });
2447
2810
  }
2811
+ if ((sub === "/release" || sub === "/discard") && req.method === "POST") {
2812
+ const body = await readBody(req);
2813
+ const ids = Array.isArray(body.ids) ? body.ids.map(String) : body.all ? null : [];
2814
+ const done = sub === "/release" ? store.release(s, ids) : store.discard(s, ids);
2815
+ if (done.length) broadcastTranscript(s, done);
2816
+ const poll = polls.get(s.id);
2817
+ if (sub === "/release" && poll && !poll.done && s.pending.length) deliver(s, poll);
2818
+ broadcastPresence(s);
2819
+ return send(res, 200, { ok: true, count: done.length });
2820
+ }
2448
2821
  if (sub === "/end" && req.method === "POST") {
2449
2822
  if (s.status !== "ended") {
2450
2823
  store.end(s, "user");
@@ -2467,7 +2840,7 @@ data: ${JSON.stringify(snapshot)}
2467
2840
  if (html == null) return send(res, 404, "artifact file is missing");
2468
2841
  try {
2469
2842
  const out = exportHtml(html);
2470
- const name = path10.basename(s.file).replace(/\.html?$/i, "") + ".export.html";
2843
+ const name = path11.basename(s.file).replace(/\.html?$/i, "") + ".export.html";
2471
2844
  return send(res, 200, out.html, {
2472
2845
  "content-type": "text/html; charset=utf-8",
2473
2846
  "content-disposition": `attachment; filename="${name.replace(/"/g, "")}"`
@@ -2483,7 +2856,7 @@ data: ${JSON.stringify(snapshot)}
2483
2856
  if (listeners.has(host)) return resolve(true);
2484
2857
  const server = http2.createServer((req, res) => {
2485
2858
  handle(req, res).catch((err) => {
2486
- log("error", req.method, req.url, String(err?.stack || err));
2859
+ log("error", req.method, req.url.split("?")[0], String(err?.stack || err));
2487
2860
  if (!res.headersSent) send(res, err.status || 500, { error: String(err.message || err) });
2488
2861
  else res.end();
2489
2862
  });
@@ -2492,7 +2865,12 @@ data: ${JSON.stringify(snapshot)}
2492
2865
  server.requestTimeout = 0;
2493
2866
  server.headersTimeout = 6e4;
2494
2867
  server.once("error", (err) => {
2495
- log("listen-failed", host, err.code || err.message);
2868
+ log(
2869
+ "listen-failed",
2870
+ host,
2871
+ /** @type {NodeJS.ErrnoException} */
2872
+ err.code || err.message
2873
+ );
2496
2874
  resolve(false);
2497
2875
  });
2498
2876
  server.listen(port2, host, () => {
@@ -2551,7 +2929,7 @@ data: ${JSON.stringify({ state: "stopped", reason })}
2551
2929
  log("started", `v${VERSION}`, `pid=${process.pid}`);
2552
2930
  return { port: port2, shutdown };
2553
2931
  }
2554
- var MIME, SHORT_GRACE_MS;
2932
+ var MIME, SHORT_GRACE_MS, OWNER, REVIEWER_KINDS;
2555
2933
  var init_server = __esm({
2556
2934
  "src/cli/server.js"() {
2557
2935
  init_artifact();
@@ -2561,6 +2939,7 @@ var init_server = __esm({
2561
2939
  init_sessions();
2562
2940
  init_packs();
2563
2941
  init_hub();
2942
+ init_invites();
2564
2943
  init_hub_page();
2565
2944
  init_packs_meta();
2566
2945
  MIME = {
@@ -2590,12 +2969,14 @@ var init_server = __esm({
2590
2969
  ".webm": "video/webm"
2591
2970
  };
2592
2971
  SHORT_GRACE_MS = 1e4;
2972
+ OWNER = Object.freeze({ role: "owner", name: "" });
2973
+ REVIEWER_KINDS = /* @__PURE__ */ new Set(["annotation", "message"]);
2593
2974
  }
2594
2975
  });
2595
2976
 
2596
2977
  // src/cli/main.js
2597
- import fs12 from "node:fs";
2598
- import path11 from "node:path";
2978
+ import fs13 from "node:fs";
2979
+ import path12 from "node:path";
2599
2980
 
2600
2981
  // node_modules/@openuidev/lang-core/dist/shared-Dzgc28Wo.mjs
2601
2982
  var TELEMETRY_REQUEST_TIMEOUT_MS = 2e3;
@@ -3456,10 +3837,10 @@ function mergeDefs(...defs) {
3456
3837
  function cloneDef(schema) {
3457
3838
  return mergeDefs(schema._zod.def);
3458
3839
  }
3459
- function getElementAtPath(obj, path12) {
3460
- if (!path12)
3840
+ function getElementAtPath(obj, path13) {
3841
+ if (!path13)
3461
3842
  return obj;
3462
- return path12.reduce((acc, key) => acc?.[key], obj);
3843
+ return path13.reduce((acc, key) => acc?.[key], obj);
3463
3844
  }
3464
3845
  function promiseAllObject(promisesObj) {
3465
3846
  const keys = Object.keys(promisesObj);
@@ -3799,11 +4180,11 @@ function explicitlyAborted(x, startIndex = 0) {
3799
4180
  }
3800
4181
  return false;
3801
4182
  }
3802
- function prefixIssues(path12, issues) {
4183
+ function prefixIssues(path13, issues) {
3803
4184
  return issues.map((iss) => {
3804
4185
  var _a3;
3805
4186
  (_a3 = iss).path ?? (_a3.path = []);
3806
- iss.path.unshift(path12);
4187
+ iss.path.unshift(path13);
3807
4188
  return iss;
3808
4189
  });
3809
4190
  }
@@ -4265,16 +4646,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
4265
4646
  }
4266
4647
  function formatError(error62, mapper = (issue2) => issue2.message) {
4267
4648
  const fieldErrors = { _errors: [] };
4268
- const processError = (error63, path12 = []) => {
4649
+ const processError = (error63, path13 = []) => {
4269
4650
  for (const issue2 of error63.issues) {
4270
4651
  if (issue2.code === "invalid_union" && issue2.errors.length) {
4271
- issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
4652
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
4272
4653
  } else if (issue2.code === "invalid_key") {
4273
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4654
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4274
4655
  } else if (issue2.code === "invalid_element") {
4275
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4656
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4276
4657
  } else {
4277
- const fullpath = [...path12, ...issue2.path];
4658
+ const fullpath = [...path13, ...issue2.path];
4278
4659
  if (fullpath.length === 0) {
4279
4660
  fieldErrors._errors.push(mapper(issue2));
4280
4661
  } else {
@@ -4313,17 +4694,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
4313
4694
  }
4314
4695
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
4315
4696
  const result = { errors: [] };
4316
- const processError = (error63, path12 = []) => {
4697
+ const processError = (error63, path13 = []) => {
4317
4698
  var _a3;
4318
4699
  for (const issue2 of error63.issues) {
4319
4700
  if (issue2.code === "invalid_union" && issue2.errors.length) {
4320
- issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
4701
+ issue2.errors.map((issues) => processError({ issues }, [...path13, ...issue2.path]));
4321
4702
  } else if (issue2.code === "invalid_key") {
4322
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4703
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4323
4704
  } else if (issue2.code === "invalid_element") {
4324
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
4705
+ processError({ issues: issue2.issues }, [...path13, ...issue2.path]);
4325
4706
  } else {
4326
- const fullpath = [...path12, ...issue2.path];
4707
+ const fullpath = [...path13, ...issue2.path];
4327
4708
  if (fullpath.length === 0) {
4328
4709
  result.errors.push(mapper(issue2));
4329
4710
  continue;
@@ -4362,8 +4743,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
4362
4743
  }
4363
4744
  function toDotPath(_path) {
4364
4745
  const segs = [];
4365
- const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4366
- for (const seg of path12) {
4746
+ const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
4747
+ for (const seg of path13) {
4367
4748
  if (typeof seg === "number")
4368
4749
  segs.push(`[${seg}]`);
4369
4750
  else if (typeof seg === "symbol")
@@ -21501,13 +21882,13 @@ function resolveRef(ref, ctx) {
21501
21882
  if (!ref.startsWith("#")) {
21502
21883
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
21503
21884
  }
21504
- const path12 = ref.slice(1).split("/").filter(Boolean);
21505
- if (path12.length === 0) {
21885
+ const path13 = ref.slice(1).split("/").filter(Boolean);
21886
+ if (path13.length === 0) {
21506
21887
  return ctx.rootSchema;
21507
21888
  }
21508
21889
  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]);
21890
+ if (path13[0] === defsKey) {
21891
+ const key = path13[1] === void 0 ? void 0 : decodeJSONPointerSegment(path13[1]);
21511
21892
  if (!key || !ctx.defs[key]) {
21512
21893
  throw new Error(`Reference not found: ${ref}`);
21513
21894
  }
@@ -22389,11 +22770,11 @@ function defineAction(config2) {
22389
22770
  ref: actionRef(config2.name, data)
22390
22771
  };
22391
22772
  }
22392
- function resolveField(obj, path12) {
22393
- if (!path12 || obj == null) return void 0;
22394
- if (!path12.includes(".")) return obj[path12];
22773
+ function resolveField(obj, path13) {
22774
+ if (!path13 || obj == null) return void 0;
22775
+ if (!path13.includes(".")) return obj[path13];
22395
22776
  let cur = obj;
22396
- for (const p of path12.split(".")) {
22777
+ for (const p of path13.split(".")) {
22397
22778
  if (cur == null) return void 0;
22398
22779
  cur = cur[p];
22399
22780
  }
@@ -23769,14 +24150,14 @@ function checkTypeMismatch(value, info) {
23769
24150
  };
23770
24151
  return null;
23771
24152
  }
23772
- function validationMessage(component, path12, issue2) {
24153
+ function validationMessage(component, path13, issue2) {
23773
24154
  switch (issue2.code) {
23774
24155
  case "type-mismatch":
23775
- return `field "${path12}" expects ${issue2.expected} but got ${issue2.actual}`;
24156
+ return `field "${path13}" expects ${issue2.expected} but got ${issue2.actual}`;
23776
24157
  case "missing-required":
23777
- return `missing required field "${path12}"${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
24158
+ return `missing required field "${path13}"${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
23778
24159
  case "null-required":
23779
- return `required field "${path12}" cannot be null${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
24160
+ return `required field "${path13}" cannot be null${issue2.signature ? ` \u2014 signature: ${issue2.signature}` : ""}`;
23780
24161
  case "unknown-component":
23781
24162
  return `Unknown component "${component}" \u2014 not found in catalog or builtins${issue2.available?.length ? `. Available components: ${issue2.available.join(", ")}` : ""}`;
23782
24163
  case "inline-reserved":
@@ -23791,12 +24172,12 @@ function validationMessage(component, path12, issue2) {
23791
24172
  return component ? `No root statement, and no statement calls the root component ${component}` : "No root statement";
23792
24173
  }
23793
24174
  }
23794
- function pushValidationIssue(ctx, component, path12, issue2) {
24175
+ function pushValidationIssue(ctx, component, path13, issue2) {
23795
24176
  ctx.errors.push({
23796
24177
  code: issue2.code,
23797
24178
  component,
23798
- path: path12,
23799
- message: validationMessage(component, path12, issue2),
24179
+ path: path13,
24180
+ message: validationMessage(component, path13, issue2),
23800
24181
  statementId: ctx.currentStatementId,
23801
24182
  ...issue2.code === "no-root" && issue2.reason === "fallback" && { severity: "warning" }
23802
24183
  });
@@ -23806,10 +24187,10 @@ function resolveInvalidValue(required2, defaultValue) {
23806
24187
  if (defaultValue !== void 0) return defaultValue;
23807
24188
  return required2 ? INVALID2 : void 0;
23808
24189
  }
23809
- function validateElementPosition(element, s, component, path12, ctx) {
24190
+ function validateElementPosition(element, s, component, path13, ctx) {
23810
24191
  if (isCompositeSchema(s)) return false;
23811
24192
  if (typeof s["type"] === "string" || Array.isArray(s["enum"]) || "const" in s) {
23812
- pushValidationIssue(ctx, component, path12, {
24193
+ pushValidationIssue(ctx, component, path13, {
23813
24194
  code: "type-mismatch",
23814
24195
  expected: typeof s["type"] === "string" ? s["type"] : "a literal value",
23815
24196
  actual: `component "${element.typeName}"`
@@ -23825,9 +24206,9 @@ function isOnlyComponentSlot(s, ctx) {
23825
24206
  const names = slotRefNames(s);
23826
24207
  return names.length > 0 && names.every((n) => n !== void 0 && ctx.cat?.has(n));
23827
24208
  }
23828
- function validateObjectValue(value, s, component, path12, ctx) {
24209
+ function validateObjectValue(value, s, component, path13, ctx) {
23829
24210
  if (typeof value !== "object" || Array.isArray(value)) {
23830
- pushValidationIssue(ctx, component, path12, {
24211
+ pushValidationIssue(ctx, component, path13, {
23831
24212
  code: "type-mismatch",
23832
24213
  expected: "object",
23833
24214
  actual: jsType(value)
@@ -23847,14 +24228,14 @@ function validateObjectValue(value, s, component, path12, ctx) {
23847
24228
  (out ??= { ...obj })[key] = fallback;
23848
24229
  continue;
23849
24230
  }
23850
- pushValidationIssue(ctx, component, `${path12}/${key}`, { code: present ? "null-required" : "missing-required" });
24231
+ pushValidationIssue(ctx, component, `${path13}/${key}`, { code: present ? "null-required" : "missing-required" });
23851
24232
  invalid = true;
23852
24233
  }
23853
24234
  }
23854
24235
  for (const key of Object.keys(props)) {
23855
24236
  if (!(key in obj)) continue;
23856
24237
  const sub = props[key];
23857
- let next = validateSchemaValue(obj[key], sub, component, `${path12}/${key}`, ctx);
24238
+ let next = validateSchemaValue(obj[key], sub, component, `${path13}/${key}`, ctx);
23858
24239
  if (next === INVALID2) {
23859
24240
  next = resolveInvalidValue(required2.includes(key), getSchemaDefaultValue(sub));
23860
24241
  if (next === INVALID2) {
@@ -23869,9 +24250,9 @@ function validateObjectValue(value, s, component, path12, ctx) {
23869
24250
  }
23870
24251
  return invalid ? INVALID2 : out ?? obj;
23871
24252
  }
23872
- function validateArrayValue(value, s, component, path12, ctx) {
24253
+ function validateArrayValue(value, s, component, path13, ctx) {
23873
24254
  if (!Array.isArray(value)) {
23874
- pushValidationIssue(ctx, component, path12, {
24255
+ pushValidationIssue(ctx, component, path13, {
23875
24256
  code: "type-mismatch",
23876
24257
  expected: "array",
23877
24258
  actual: jsType(value)
@@ -23882,19 +24263,19 @@ function validateArrayValue(value, s, component, path12, ctx) {
23882
24263
  if (!items || typeof items !== "object" || Array.isArray(items)) return value;
23883
24264
  let out;
23884
24265
  for (let i = 0; i < value.length; i++) {
23885
- const next = validateSchemaValue(value[i], items, component, `${path12}/${i}`, ctx);
24266
+ const next = validateSchemaValue(value[i], items, component, `${path13}/${i}`, ctx);
23886
24267
  if (next !== value[i] && !out) out = value.slice(0, i);
23887
24268
  if (out && next !== INVALID2) out.push(next);
23888
24269
  }
23889
24270
  return out ?? value;
23890
24271
  }
23891
- function validateLeafValue(value, s, component, path12, ctx) {
24272
+ function validateLeafValue(value, s, component, path13, ctx) {
23892
24273
  const leaf = getScalarTypeInfo(s);
23893
24274
  if (leaf.expectedType == null && leaf.enumValues == null) return false;
23894
24275
  if (leaf.enumValues && ctx.partial) return false;
23895
24276
  const mismatch = checkTypeMismatch(value, leaf);
23896
24277
  if (mismatch) {
23897
- pushValidationIssue(ctx, component, path12, {
24278
+ pushValidationIssue(ctx, component, path13, {
23898
24279
  code: "type-mismatch",
23899
24280
  ...mismatch
23900
24281
  });
@@ -23902,15 +24283,15 @@ function validateLeafValue(value, s, component, path12, ctx) {
23902
24283
  }
23903
24284
  return false;
23904
24285
  }
23905
- function validateSchemaValue(value, schema, component, path12, ctx) {
24286
+ function validateSchemaValue(value, schema, component, path13, ctx) {
23906
24287
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) return value;
23907
24288
  const s = schema;
23908
24289
  if (value == null) return value;
23909
24290
  if (isASTNode(value)) return value;
23910
- if (isElementNode(value)) return validateElementPosition(value, s, component, path12, ctx) ? INVALID2 : value;
24291
+ if (isElementNode(value)) return validateElementPosition(value, s, component, path13, ctx) ? INVALID2 : value;
23911
24292
  if (isCompositeSchema(s)) {
23912
24293
  if (!isOnlyComponentSlot(s, ctx)) return value;
23913
- pushValidationIssue(ctx, component, path12, {
24294
+ pushValidationIssue(ctx, component, path13, {
23914
24295
  code: "type-mismatch",
23915
24296
  expected: slotRefNames(s).join(" | "),
23916
24297
  actual: Array.isArray(value) ? "array" : typeof value === "object" ? "plain object" : typeof value
@@ -23918,9 +24299,9 @@ function validateSchemaValue(value, schema, component, path12, ctx) {
23918
24299
  return INVALID2;
23919
24300
  }
23920
24301
  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;
24302
+ if (type === "object") return validateObjectValue(value, s, component, path13, ctx);
24303
+ if (type === "array") return validateArrayValue(value, s, component, path13, ctx);
24304
+ return validateLeafValue(value, s, component, path13, ctx) ? INVALID2 : value;
23924
24305
  }
23925
24306
  function containsDynamicValue(v) {
23926
24307
  if (v == null || typeof v !== "object") return false;
@@ -27542,11 +27923,11 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
27542
27923
  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
27924
  if (!blocked) {
27544
27925
  routes.set(e.id, [a, b]);
27545
- const path13 = [];
27926
+ const path14 = [];
27546
27927
  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);
27928
+ if (j0 === j1) for (let i = Math.min(i0, i1); i <= Math.max(i0, i1); i++) path14.push([i, j0]);
27929
+ else for (let j = Math.min(j0, j1); j <= Math.max(j0, j1); j++) path14.push([i0, j]);
27930
+ if (path14.every(([i, j]) => i !== void 0 && j !== void 0)) mark(path14);
27550
27931
  continue;
27551
27932
  }
27552
27933
  }
@@ -27556,12 +27937,12 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
27556
27937
  const sj = iy(st2.y);
27557
27938
  const ti = ix(tt2.x);
27558
27939
  const tj = iy(tt2.y);
27559
- let path12 = null;
27940
+ let path13 = null;
27560
27941
  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)]);
27942
+ path13 = search(si, sj, dirOf(e.fromSide), ti, tj, reverse[dirOf(e.toSide)]);
27562
27943
  }
27563
- const pts = path12 ? path12.map(([i, j]) => ({ x: xs[i], y: ys[j] })) : elbow(st2, tt2, e.fromSide);
27564
- if (path12) mark(path12);
27944
+ const pts = path13 ? path13.map(([i, j]) => ({ x: xs[i], y: ys[j] })) : elbow(st2, tt2, e.fromSide);
27945
+ if (path13) mark(path13);
27565
27946
  routes.set(e.id, simplify([e.fromPort, ...pts, e.toPort]));
27566
27947
  }
27567
27948
  return routes;
@@ -27630,10 +28011,10 @@ function routeOrthogonal({ boxes, edges, xLines = [], yLines = [], avoid = { xs:
27630
28011
  }
27631
28012
  return out.reverse();
27632
28013
  }
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];
28014
+ function mark(path13) {
28015
+ for (let k = 1; k < path13.length; k++) {
28016
+ const [i0, j0] = path13[k - 1];
28017
+ const [i1, j1] = path13[k];
27637
28018
  if (j0 === j1) {
27638
28019
  hUse[j0 * nx + Math.min(i0, i1)]++;
27639
28020
  hPass[j0 * nx + i0]++;
@@ -28320,7 +28701,7 @@ import fs5 from "node:fs";
28320
28701
  import http from "node:http";
28321
28702
  import path4 from "node:path";
28322
28703
  function wantHub() {
28323
- return hubEnabled() && !hubUnsupported();
28704
+ return hubEnabled();
28324
28705
  }
28325
28706
  function request(method, route, body, { timeoutMs = 0, port: port2 = port() } = {}) {
28326
28707
  return new Promise((resolve, reject) => {
@@ -28379,11 +28760,16 @@ function spawnServer(port2, alsoListen = []) {
28379
28760
  env: { ...process.env, OPENUI_TELEMETRY_DISABLED: "1" },
28380
28761
  windowsHide: true
28381
28762
  });
28763
+ let exited = null;
28764
+ child.on("exit", (code) => exited = code ?? "signal");
28382
28765
  child.unref();
28766
+ return () => exited;
28383
28767
  }
28768
+ var START_TIMEOUT_MS = 3e4;
28384
28769
  async function ensureServer() {
28385
28770
  const port2 = port();
28386
28771
  let h = await health(port2);
28772
+ let exited = () => null;
28387
28773
  if (h?.foreign) {
28388
28774
  throw new Error(`port ${port2} is in use by something that is not austere-axi; set AUSTERE_AXI_PORT to another port`);
28389
28775
  }
@@ -28396,15 +28782,21 @@ async function ensureServer() {
28396
28782
  });
28397
28783
  for (let i = 0; i < 40 && await health(port2); i++) await sleep(100);
28398
28784
  h = null;
28399
- spawnServer(port2, keep);
28785
+ exited = spawnServer(port2, keep);
28400
28786
  } else if (!h) {
28401
- spawnServer(port2);
28787
+ exited = spawnServer(port2);
28402
28788
  }
28403
28789
  if (!h) {
28404
- for (let i = 0; i < 80; i++) {
28790
+ const deadline = Date.now() + START_TIMEOUT_MS;
28791
+ while (Date.now() < deadline) {
28405
28792
  await sleep(100);
28406
28793
  h = await health(port2);
28407
28794
  if (h && !h.foreign) return h;
28795
+ if (exited() != null) {
28796
+ h = await health(port2);
28797
+ if (h && !h.foreign) return h;
28798
+ break;
28799
+ }
28408
28800
  }
28409
28801
  throw new Error(`server did not start on port ${port2}; see ${path4.join(stateDir(), "server.log")}`);
28410
28802
  }
@@ -28441,6 +28833,7 @@ function openBrowser(url2) {
28441
28833
 
28442
28834
  // src/cli/main.js
28443
28835
  init_hub();
28836
+ init_invites();
28444
28837
  init_sessions();
28445
28838
  init_export();
28446
28839
 
@@ -28689,13 +29082,16 @@ COMMANDS
28689
29082
  (check --fix writes the <meta name="austere-packs"> tag)
28690
29083
  ${BIN2} components new <dir> | build [dir] write your own pack
28691
29084
  ${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
29085
+ ${BIN2} hub on|off|status|list|notes|open keep the server up, indexing every artifact by project/date
29086
+ ${BIN2} share <file> --as "<name>" [--auto] invite link for a teammate on your tailnet. Their notes
29087
+ wait for the owner to send them on; --auto skips the wait
29088
+ ${BIN2} share list [--links] | revoke <name|id>
28693
29089
  ${BIN2} stop stop the background server
28694
29090
 
28695
29091
  ENV
28696
29092
  AUSTERE_AXI_PORT (4388) AUSTERE_AXI_STATE_DIR (~/.austere-axi) AUSTERE_AXI_NO_OPEN=1
28697
29093
  AUSTERE_AXI_HOST AUSTERE_AXI_NO_TAILSCALE=1 AUSTERE_AXI_DISCONNECT_GRACE_MS (300000)
28698
- AUSTERE_AXI_IDLE_TIMEOUT_MS (1800000) AUSTERE_AXI_ALLOWED_HOSTS AUSTERE_AXI_NPM_REGISTRY
29094
+ AUSTERE_AXI_IDLE_TIMEOUT_MS (1800000) AUSTERE_AXI_ALLOWED_HOSTS AUSTERE_AXI_LINK_URL AUSTERE_AXI_NPM_REGISTRY
28699
29095
  `;
28700
29096
  function parseArgs(argv) {
28701
29097
  const flags = {};
@@ -28709,7 +29105,7 @@ function parseArgs(argv) {
28709
29105
  if (a.startsWith("--")) {
28710
29106
  const eq = a.indexOf("=");
28711
29107
  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);
29108
+ const takesValue = ["agent-reply", "agent-reply-file", "owner", "out", "port", "also-listen", "timeout-ms", "name", "project", "as"].includes(key);
28713
29109
  let val = true;
28714
29110
  if (eq > 0) val = a.slice(eq + 1);
28715
29111
  else if (takesValue) val = argv[++i];
@@ -28731,7 +29127,7 @@ function needFile(file2, cmd) {
28731
29127
  return null;
28732
29128
  }
28733
29129
  const abs = canonicalFile(file2);
28734
- if (!fs12.existsSync(abs)) {
29130
+ if (!fs13.existsSync(abs)) {
28735
29131
  fail(`no such file: ${abs}`);
28736
29132
  return null;
28737
29133
  }
@@ -28748,7 +29144,7 @@ function readStdin() {
28748
29144
  async function replyText(flags) {
28749
29145
  if (typeof flags["agent-reply-file"] === "string") {
28750
29146
  const f = flags["agent-reply-file"];
28751
- return (f === "-" ? await readStdin() : fs12.readFileSync(f, "utf8")).trim();
29147
+ return (f === "-" ? await readStdin() : fs13.readFileSync(f, "utf8")).trim();
28752
29148
  }
28753
29149
  if (typeof flags["agent-reply"] === "string") return flags["agent-reply"].trim();
28754
29150
  return "";
@@ -28779,7 +29175,7 @@ async function cmdHome() {
28779
29175
  async function cmdOpen(file2, flags) {
28780
29176
  const abs = needFile(file2, "open");
28781
29177
  if (!abs) return;
28782
- const html = fs12.readFileSync(abs, "utf8");
29178
+ const html = fs13.readFileSync(abs, "utf8");
28783
29179
  const check2 = checkHtml(html, { file: quote(file2) });
28784
29180
  await ensureServer();
28785
29181
  const r = await request("POST", "/api/open", { file: abs, reopen: !!flags.reopen });
@@ -28807,6 +29203,7 @@ async function cmdOpen(file2, flags) {
28807
29203
  check: compactCheck(check2)
28808
29204
  };
28809
29205
  if (d.pending) out.queued_feedback = d.pending;
29206
+ if (d.held) out.held_notes = `${d.held} from teammates, waiting for the owner to send them`;
28810
29207
  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
29208
  print(out);
28812
29209
  }
@@ -28816,7 +29213,7 @@ function quote(file2) {
28816
29213
  function describeItems(abs, items) {
28817
29214
  let lines = /* @__PURE__ */ new Map();
28818
29215
  try {
28819
- lines = statementLines(fs12.readFileSync(abs, "utf8"));
29216
+ lines = statementLines(fs13.readFileSync(abs, "utf8"));
28820
29217
  } catch {
28821
29218
  }
28822
29219
  const at2 = (id) => {
@@ -28826,31 +29223,35 @@ function describeItems(abs, items) {
28826
29223
  return { line: l.line, source: first.length > 110 ? first.slice(0, 109) + "\u2026" : first };
28827
29224
  };
28828
29225
  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 ?? "" };
29226
+ const o = describeItem(it2, at2);
29227
+ return it2.author ? { from: it2.author, ...o } : o;
28849
29228
  });
28850
29229
  }
29230
+ function describeItem(it2, at2) {
29231
+ if (it2.kind === "annotation") {
29232
+ const o = { annotation: `${it2.target} (${it2.component})`, ...at2(it2.target) };
29233
+ const within = (it2.path || []).slice(0, -1).filter((p) => p !== "root");
29234
+ if (within.length) o.within = within.join(" > ");
29235
+ if (it2.selection) o.selection = it2.selection;
29236
+ else if (it2.excerpt) o.excerpt = it2.excerpt;
29237
+ if (it2.detail) o.detail = it2.detail;
29238
+ o.note = it2.note || "(no note: the selection itself is the point)";
29239
+ return o;
29240
+ }
29241
+ if (it2.kind === "action") {
29242
+ const o = { action: it2.message };
29243
+ if (it2.form) o.form = it2.form;
29244
+ if (it2.context !== void 0) o.context = it2.context;
29245
+ return o;
29246
+ }
29247
+ if (it2.kind === "render-error") {
29248
+ return { render_error: it2.statement || "(page)", ...at2(it2.statement), error: it2.message, note: it2.note };
29249
+ }
29250
+ return { message: it2.text ?? it2.note ?? "" };
29251
+ }
28851
29252
  function thirdPartyOf(abs) {
28852
29253
  try {
28853
- return pagePacks(fs12.readFileSync(abs, "utf8")).thirdParty.map((p) => `${p.name}@${p.version}`);
29254
+ return pagePacks(fs13.readFileSync(abs, "utf8")).thirdParty.map((p) => `${p.name}@${p.version}`);
28854
29255
  } catch {
28855
29256
  return [];
28856
29257
  }
@@ -28872,7 +29273,7 @@ async function cmdPoll(file2, flags) {
28872
29273
  if (!reply && (flags["agent-reply"] !== void 0 || flags["agent-reply-file"] !== void 0)) {
28873
29274
  return fail("the agent reply is empty");
28874
29275
  }
28875
- process.stderr.write(`[${BIN2}] waiting for feedback on ${path11.basename(abs)} (interrupting is safe: feedback stays queued)
29276
+ process.stderr.write(`[${BIN2}] waiting for feedback on ${path12.basename(abs)} (interrupting is safe: feedback stays queued)
28876
29277
  `);
28877
29278
  const ticker = process.stderr.isTTY ? setInterval(() => process.stderr.write(`[${BIN2}] still waiting\u2026
28878
29279
  `), 6e4) : null;
@@ -28910,7 +29311,7 @@ async function cmdPoll(file2, flags) {
28910
29311
  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
29312
  }
28912
29313
  if (d.ended) out2.ended = `by ${d.endedBy || "user"}`;
28913
- out2.next = d.ended ? NEXT.feedbackEnded(f) : NEXT.feedback(f);
29314
+ out2.next = d.ended ? NEXT.feedbackEnded() : NEXT.feedback(f);
28914
29315
  return print(out2);
28915
29316
  }
28916
29317
  const out = { status: d.status || "unknown", file: abs };
@@ -28955,13 +29356,13 @@ function fixPacks(abs, html) {
28955
29356
  return { html, changes };
28956
29357
  }
28957
29358
  const out = setPacksMeta(html, next);
28958
- if (out !== html) fs12.writeFileSync(abs, out);
29359
+ if (out !== html) fs13.writeFileSync(abs, out);
28959
29360
  return { html: out, changes: changes.length ? changes : ["updated integrity hashes"] };
28960
29361
  }
28961
29362
  async function cmdCheck(file2, flags = {}) {
28962
29363
  const abs = needFile(file2, "check");
28963
29364
  if (!abs) return;
28964
- let html = fs12.readFileSync(abs, "utf8");
29365
+ let html = fs13.readFileSync(abs, "utf8");
28965
29366
  let fixed = [];
28966
29367
  if (flags.fix) ({ html, changes: fixed } = fixPacks(abs, html));
28967
29368
  const result = checkHtml(html, { file: quote(file2) });
@@ -28980,12 +29381,12 @@ async function cmdPatch(file2) {
28980
29381
  if (process.stdin.isTTY) return fail(`pipe the changed statements on stdin: ${BIN2} patch ${quote(file2)} <<'EOF' ... EOF`);
28981
29382
  const patch = await readStdin();
28982
29383
  if (!patch.trim()) return fail("stdin was empty");
28983
- const html = fs12.readFileSync(abs, "utf8");
29384
+ const html = fs13.readFileSync(abs, "utf8");
28984
29385
  const art = readArtifact(html);
28985
29386
  if (art.program == null) return fail('no <script type="text/openui"> block to patch');
28986
29387
  const merged = mergeStatements(art.program.trim(), patch.trim(), "root");
28987
29388
  const next = replaceProgram(html, merged);
28988
- fs12.writeFileSync(abs, next);
29389
+ fs13.writeFileSync(abs, next);
28989
29390
  const result = checkHtml(next, { file: quote(file2) });
28990
29391
  await registerWithHub(abs, result.title, sessionId(abs));
28991
29392
  print({ status: "patched", file: abs, check: compactCheck(result) });
@@ -28998,7 +29399,7 @@ function packSpec(p) {
28998
29399
  return lines.join("\n") + "\n";
28999
29400
  }
29000
29401
  function cmdSpec(groups) {
29001
- let text = fs12.readFileSync(assetPath("prompt.md"), "utf8");
29402
+ let text = fs13.readFileSync(assetPath("prompt.md"), "utf8");
29002
29403
  const packs = installedPacks();
29003
29404
  const packArgs = groups.filter((g) => packs.some((p) => p.name === g));
29004
29405
  if (packArgs.length) {
@@ -29032,9 +29433,9 @@ ${trimmed.join("\n")}
29032
29433
  function cmdExport(file2, flags) {
29033
29434
  const abs = needFile(file2, "export");
29034
29435
  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);
29436
+ const out = typeof flags.out === "string" ? path12.resolve(flags.out) : abs.replace(/\.html?$/i, "") + ".export.html";
29437
+ const { html, notices } = exportHtml(fs13.readFileSync(abs, "utf8"));
29438
+ fs13.writeFileSync(out, html);
29038
29439
  print({ status: "exported", file: out, bytes: html.length, notices: notices.length ? notices : void 0 });
29039
29440
  }
29040
29441
  function printResult(out) {
@@ -29093,7 +29494,7 @@ async function cmdComponents(sub, args, flags) {
29093
29494
  pack: r.name,
29094
29495
  dir: r.dir,
29095
29496
  files: r.files,
29096
- next: `cd ${quote(path11.relative(process.cwd(), r.dir) || ".")} && npm install && npm run build, then \`${BIN2} components add .\``
29497
+ next: `cd ${quote(path12.relative(process.cwd(), r.dir) || ".")} && npm install && npm run build, then \`${BIN2} components add .\``
29097
29498
  });
29098
29499
  } catch (err) {
29099
29500
  return fail(err.message);
@@ -29168,7 +29569,7 @@ async function cmdFeedback(file2, flags) {
29168
29569
  }
29169
29570
  const items = [];
29170
29571
  for (const e of entries) {
29171
- for (const it2 of describeItems(abs, e.items || [])) {
29572
+ for (const it2 of describeItems(abs, (e.items || []).map((x) => e.author ? { ...x, author: e.author } : x))) {
29172
29573
  items.push(opts.all ? { sent: shortDate(e.at), status: e.status, ...it2 } : { sent: shortDate(e.at), ...it2 });
29173
29574
  }
29174
29575
  }
@@ -29185,8 +29586,6 @@ async function cmdHub(sub, args, flags) {
29185
29586
  const url2 = `http://127.0.0.1:${port()}/`;
29186
29587
  switch (sub) {
29187
29588
  case "on": {
29188
- const why = hubUnsupported();
29189
- if (why) return fail(why);
29190
29589
  setHubEnabled(true);
29191
29590
  let h = await ensureServer();
29192
29591
  if (h.hubWanted && !h.hub) {
@@ -29199,14 +29598,14 @@ async function cmdHub(sub, args, flags) {
29199
29598
  process.exitCode = 1;
29200
29599
  return print({
29201
29600
  status: "unavailable",
29202
- error: `the hub could not open its index (${hubDbFile()}); see ${path11.join(stateDir(), "server.log")}`,
29601
+ error: `the hub could not open its index (${hubDbFile()}); see ${path12.join(stateDir(), "server.log")}`,
29203
29602
  note: "Reviews still work; the server idles out as usual. Run `austere-axi hub on` again once the index can be opened."
29204
29603
  });
29205
29604
  }
29206
29605
  return print({
29207
29606
  status: "on",
29208
29607
  url: url2,
29209
- phone: h.tailnet ? `http://${h.tailnet.dnsName || h.tailnet.ip}:${port()}/` : void 0,
29608
+ phone: h.remote ? `${h.remote}/` : h.tailnet ? `http://${h.tailnet.dnsName || h.tailnet.ip}:${port()}/` : void 0,
29210
29609
  index: hubDbFile(),
29211
29610
  note: "The server now stays up and indexes every artifact you open or check, by project and date. After a reboot, `austere-axi hub open` (or opening any review) starts it again."
29212
29611
  });
@@ -29223,12 +29622,10 @@ async function cmdHub(sub, args, flags) {
29223
29622
  const h = await serverIfRunning();
29224
29623
  const out = {
29225
29624
  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"
29625
+ server: h ? `running on ${port()}${h.hub ? " (hub)" : h.hubWanted ? ` (hub could not start; see ${path12.join(stateDir(), "server.log")})` : ""}` : "not running"
29227
29626
  };
29228
- const why = hubUnsupported();
29229
- if (why) out.unavailable = why;
29230
29627
  if (hubEnabled() && h?.hub) out.url = url2;
29231
- if (!why && fs12.existsSync(hubDbFile())) {
29628
+ if (fs13.existsSync(hubDbFile())) {
29232
29629
  try {
29233
29630
  const hub = await Hub.open();
29234
29631
  try {
@@ -29240,14 +29637,12 @@ async function cmdHub(sub, args, flags) {
29240
29637
  out.index = `${hubDbFile()} cannot be opened: ${err.message}`;
29241
29638
  }
29242
29639
  }
29243
- if (!hubEnabled()) out.next = `\`${BIN2} hub on\` keeps the server up with an index of every artifact (Node ${HUB_MIN_NODE}+).`;
29640
+ if (!hubEnabled()) out.next = `\`${BIN2} hub on\` keeps the server up with an index of every artifact.`;
29244
29641
  return print(out);
29245
29642
  }
29246
29643
  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) : "";
29644
+ if (!fs13.existsSync(hubDbFile())) return print({ artifacts: "none", next: `\`${BIN2} hub on\` starts indexing.` });
29645
+ const project = typeof flags.project === "string" ? path12.resolve(flags.project) : "";
29251
29646
  let hub;
29252
29647
  try {
29253
29648
  hub = await Hub.open();
@@ -29271,14 +29666,102 @@ async function cmdHub(sub, args, flags) {
29271
29666
  next: `\`${BIN2} feedback <file>\` for what a reviewer said that no agent has answered.`
29272
29667
  });
29273
29668
  }
29669
+ case "notes": {
29670
+ if (!fs13.existsSync(hubDbFile())) return print({ notes: "none", next: `\`${BIN2} hub on\` starts indexing.` });
29671
+ const project = typeof flags.project === "string" ? path12.resolve(flags.project) : "";
29672
+ let hub;
29673
+ try {
29674
+ hub = await Hub.open();
29675
+ } catch (err) {
29676
+ return fail(`${hubDbFile()} cannot be opened: ${err.message}`);
29677
+ }
29678
+ let rows;
29679
+ try {
29680
+ rows = hub.notes({ q: args.join(" "), project, status: flags.unanswered ? "unanswered" : "", limit: 40 });
29681
+ } finally {
29682
+ hub.close();
29683
+ }
29684
+ if (!rows.length) return print({ notes: "none" });
29685
+ return print({
29686
+ notes: rows.map((r) => ({
29687
+ from: r.author || void 0,
29688
+ text: r.text || void 0,
29689
+ target: r.target || void 0,
29690
+ status: r.status === "addressed" ? "answered" : r.status === "held" ? "held" : "unanswered",
29691
+ page: r.title || path12.basename(r.file),
29692
+ file: r.file,
29693
+ sent: shortDate(r.at)
29694
+ })),
29695
+ next: `\`${BIN2} feedback <file>\` for what a reviewer said that no agent has answered.`
29696
+ });
29697
+ }
29274
29698
  case "open": {
29275
29699
  if (!wantHub()) return fail(`the hub is off: \`${BIN2} hub on\``);
29276
29700
  await ensureServer();
29277
29701
  return print({ status: openBrowser(url2) ? "opened" : "open the url", url: url2 });
29278
29702
  }
29279
29703
  default:
29280
- return fail(`unknown hub command: ${sub}. One of: on off status list open`);
29704
+ return fail(`unknown hub command: ${sub}. One of: on off status list notes open`);
29705
+ }
29706
+ }
29707
+ function ago(ms) {
29708
+ return shortDate(ms);
29709
+ }
29710
+ async function cmdShare(args, flags) {
29711
+ const [sub, ...more] = args;
29712
+ if (sub === "list") {
29713
+ let invites;
29714
+ const withLinks = Boolean(flags.links);
29715
+ if (await serverIfRunning()) {
29716
+ const r2 = await request("GET", `/api/share/list${withLinks ? "?links=1" : ""}`);
29717
+ if (r2.status !== 200) return fail(r2.data?.error || `share list failed (${r2.status})`);
29718
+ invites = r2.data.invites;
29719
+ } else {
29720
+ invites = new InviteStore(stateDir()).list().map((i) => ({ id: inviteId(i), name: i.name, file: i.file, auto: i.auto, createdAt: i.createdAt }));
29721
+ }
29722
+ if (!invites.length) return print({ invites: "none", next: `${BIN2} share <file> --as "<name>"` });
29723
+ return print({
29724
+ invites: invites.map((i) => ({
29725
+ name: i.name,
29726
+ id: i.id,
29727
+ file: i.file,
29728
+ mode: i.auto ? "auto" : "hold",
29729
+ created: ago(i.createdAt),
29730
+ link: i.link
29731
+ })),
29732
+ next: withLinks ? void 0 : `--links shows the links. ${BIN2} share revoke <name|id> cuts one off.`
29733
+ });
29281
29734
  }
29735
+ if (sub === "revoke") {
29736
+ const key = more.join(" ");
29737
+ if (!key) return fail(`revoke needs a name or id: ${BIN2} share revoke <name|id> (see ${BIN2} share list)`);
29738
+ let revoked;
29739
+ if (await serverIfRunning()) {
29740
+ const r2 = await request("POST", "/api/share/revoke", { key });
29741
+ if (r2.status !== 200) return fail(r2.data?.error || `revoke failed (${r2.status})`);
29742
+ revoked = r2.data.revoked;
29743
+ } else {
29744
+ revoked = new InviteStore(stateDir()).revoke(key).map((i) => ({ id: inviteId(i), name: i.name, file: i.file }));
29745
+ if (!revoked.length) return fail("no invite matches that name or id");
29746
+ }
29747
+ return print({ status: "revoked", invites: revoked.map((i) => `${i.name} (${i.id})`), note: "Their link and open pages stop working at once." });
29748
+ }
29749
+ const abs = needFile(sub, "share");
29750
+ if (!abs) return;
29751
+ if (typeof flags.as !== "string" || !flags.as.trim()) return fail(`share needs a reviewer name: ${BIN2} share ${quote(sub)} --as "<name>"`);
29752
+ await ensureServer();
29753
+ const r = await request("POST", "/api/share", { file: abs, name: flags.as, auto: !!flags.auto });
29754
+ if (r.status !== 200) return fail(r.data?.error || `share failed (${r.status})`);
29755
+ const d = r.data;
29756
+ print({
29757
+ status: "shared",
29758
+ reviewer: d.name,
29759
+ file: abs,
29760
+ mode: d.auto ? "auto: their notes go straight to the agent" : "hold: their notes wait until the owner sends them on",
29761
+ link: d.link,
29762
+ note: "Anyone with this link on your tailnet can post notes under this name until you revoke it.",
29763
+ next: `${BIN2} share revoke ${JSON.stringify(d.name)} cuts it off.`
29764
+ });
29282
29765
  }
29283
29766
  async function cmdStop() {
29284
29767
  const h = await serverIfRunning();
@@ -29292,7 +29775,12 @@ async function cmdServer(flags) {
29292
29775
  const p = flags.port ? Number(flags.port) : port();
29293
29776
  await runServer2({ port: p, verbose: !!flags.verbose, alsoListen: flags["also-listen"] || [] });
29294
29777
  }
29778
+ var MIN_NODE = [22, 18];
29295
29779
  async function main() {
29780
+ const [major, minor] = process.versions.node.split(".").map(Number);
29781
+ if (major < MIN_NODE[0] || major === MIN_NODE[0] && minor < MIN_NODE[1]) {
29782
+ return fail(`austere-axi needs Node ${MIN_NODE.join(".")} or newer; this is ${process.versions.node}. Install a current Node LTS.`);
29783
+ }
29296
29784
  const { flags, pos } = parseArgs(process.argv.slice(2));
29297
29785
  if (flags.version) return process.stdout.write(VERSION + "\n");
29298
29786
  const [cmd, ...rest] = pos;
@@ -29330,6 +29818,8 @@ Replies render a small Markdown subset (paragraphs, - lists, \`code\`, **bold**,
29330
29818
  return cmdSpec(rest);
29331
29819
  case "export":
29332
29820
  return cmdExport(rest[0], flags);
29821
+ case "share":
29822
+ return cmdShare(rest, flags);
29333
29823
  case "stop":
29334
29824
  return cmdStop();
29335
29825
  case "server":
@@ -29339,7 +29829,7 @@ Replies render a small Markdown subset (paragraphs, - lists, \`code\`, **bold**,
29339
29829
  case "help":
29340
29830
  return process.stdout.write(HELP);
29341
29831
  default:
29342
- if (/\.html?$/i.test(cmd) || fs12.existsSync(cmd)) return cmdOpen(cmd, flags);
29832
+ if (/\.html?$/i.test(cmd) || fs13.existsSync(cmd)) return cmdOpen(cmd, flags);
29343
29833
  return fail(`unknown command: ${cmd}. Run ${BIN2} --help`);
29344
29834
  }
29345
29835
  }