@bonginkan/maria-lite 6.3.0 → 6.3.2

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.cjs CHANGED
@@ -45614,7 +45614,7 @@ var require_src8 = __commonJS({
45614
45614
  */
45615
45615
  }, {
45616
45616
  key: "getToken",
45617
- value: function getToken2(callback) {
45617
+ value: function getToken3(callback) {
45618
45618
  var opts = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
45619
45619
  if (_typeof(callback) === "object") {
45620
45620
  opts = callback;
@@ -97578,7 +97578,7 @@ function resolvePackageJsonNearEntrypoint() {
97578
97578
  }
97579
97579
  function resolveMariaLiteVersionInfo() {
97580
97580
  const fallbackName = EXPECTED_PKG_NAME;
97581
- const fallbackVersion = String(process.env.MARIA_LITE_VERSION || "").trim() || "6.3.0";
97581
+ const fallbackVersion = String(process.env.MARIA_LITE_VERSION || "").trim() || "6.3.2";
97582
97582
  const near = resolvePackageJsonNearEntrypoint();
97583
97583
  if (near) {
97584
97584
  const name = fallbackName;
@@ -122583,7 +122583,7 @@ async function handleToolCalls(params) {
122583
122583
  ...written.map((w) => `- ${w.rel}`)
122584
122584
  ].join("\n");
122585
122585
  await params.ctxStore.appendCommon({ actorId: params.actorId, text: header });
122586
- const clamp = (s2, max) => {
122586
+ const clamp2 = (s2, max) => {
122587
122587
  const t2 = String(s2 || "");
122588
122588
  if (t2.length <= max) return t2;
122589
122589
  return `${t2.slice(0, max)}
@@ -122599,15 +122599,15 @@ async function handleToolCalls(params) {
122599
122599
  if (fileRel) lines.push(`file=${fileRel}`);
122600
122600
  lines.push("");
122601
122601
  lines.push("description:");
122602
- lines.push(clamp(p.description || "", 2e3));
122602
+ lines.push(clamp2(p.description || "", 2e3));
122603
122603
  lines.push("");
122604
122604
  lines.push("entries:");
122605
122605
  for (const e2 of p.entries.slice(0, 20)) {
122606
122606
  lines.push(`- id=${e2.id} kind=${e2.kind} importance=${e2.importance} title=${e2.title}`);
122607
122607
  if (e2.tags && e2.tags.length) lines.push(` tags=${e2.tags.slice().sort((a, b) => a.localeCompare(b)).join(",")}`);
122608
- if (e2.summary) lines.push(` summary=${clamp(e2.summary, 800).replaceAll("\n", " ")}`);
122608
+ if (e2.summary) lines.push(` summary=${clamp2(e2.summary, 800).replaceAll("\n", " ")}`);
122609
122609
  if (e2.body) lines.push(` body=
122610
- ${clamp(e2.body, 2200)}`);
122610
+ ${clamp2(e2.body, 2200)}`);
122611
122611
  }
122612
122612
  await params.ctxStore.appendCommon({ actorId: params.actorId, text: lines.join("\n") });
122613
122613
  }
@@ -127245,7 +127245,7 @@ function categorizeCommand(id) {
127245
127245
  const dataSet = /* @__PURE__ */ new Set(["kp-sync", "kp-generate", "kp-update", "kp-synthesis", "import-origin", "reorganize", "origin-fetch"]);
127246
127246
  const creativeSet = /* @__PURE__ */ new Set(["blog", "slides", "docs", "proposal", "pdf", "manga", "novel", "film", "spreadsheet"]);
127247
127247
  const academicSet = /* @__PURE__ */ new Set(["paper", "exam"]);
127248
- const googleSet = /* @__PURE__ */ new Set(["gmail", "gcal", "gdrive", "gmeet"]);
127248
+ const googleSet = /* @__PURE__ */ new Set(["gmail", "gcal", "gdrive", "gmeet", "resource-manager"]);
127249
127249
  const phoneSet = /* @__PURE__ */ new Set(["phone-tenant", "phone-dept", "phone-prompt", "phone-dict", "phone-hp-import", "phone-deploy", "phone-init"]);
127250
127250
  const systemSet = /* @__PURE__ */ new Set(["help", "version", "gui", "desktop", "login", "logout", "account", "vup", "schedule", "task-manager", "log-viewer", "google-connect"]);
127251
127251
  if (dailySet.has(id)) return "daily";
@@ -127368,7 +127368,8 @@ function commandIcon(id) {
127368
127368
  "competitors": "\u{1F3C6}",
127369
127369
  "task-distribution": "\u{1F4CB}",
127370
127370
  "billing-pl": "\u{1F4B0}",
127371
- "dev-adviser": "\u{1F468}\u200D\u{1F4BB}"
127371
+ "dev-adviser": "\u{1F468}\u200D\u{1F4BB}",
127372
+ "resource-manager": "\u{1F3E2}"
127372
127373
  };
127373
127374
  return icons[id] || "\u{1F4CE}";
127374
127375
  }
@@ -192002,7 +192003,7 @@ function normalizeImageCli(ctx) {
192002
192003
  const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
192003
192004
  const applyExplicit = hasLiteFlag(ctx.parsed, "apply");
192004
192005
  const apply = planOnly ? false : applyExplicit || true;
192005
- const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : "outputs/images";
192006
+ const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : ".maria/desktop/images";
192006
192007
  const outPathRel = normalizeRelOutPath(ctx.parsed.options.path, `.${format2}`) || void 0;
192007
192008
  const concurrency = ctx.parsed.options.concurrency ? Math.max(1, Math.min(16, Math.floor(Number(ctx.parsed.options.concurrency)))) : void 0;
192008
192009
  const retry = ctx.parsed.options.retry ? Math.max(0, Math.min(10, Math.floor(Number(ctx.parsed.options.retry)))) : void 0;
@@ -192335,7 +192336,7 @@ function normalizeVideoCli(ctx) {
192335
192336
  const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
192336
192337
  const applyExplicit = hasLiteFlag(ctx.parsed, "apply");
192337
192338
  const apply = planOnly ? false : applyExplicit || true;
192338
- const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : "outputs/videos";
192339
+ const outDirRel = ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : ".maria/desktop/videos";
192339
192340
  const outPathRel = normalizeRelOutPath2(ctx.parsed.options.path, `.${format2}`) || void 0;
192340
192341
  const concurrency = ctx.parsed.options.concurrency ? Math.max(1, Math.min(16, Math.floor(Number(ctx.parsed.options.concurrency)))) : void 0;
192341
192342
  const retry = ctx.parsed.options.retry ? Math.max(0, Math.min(10, Math.floor(Number(ctx.parsed.options.retry)))) : void 0;
@@ -193314,8 +193315,8 @@ function parseSlidesTextShiftOpt(raw) {
193314
193315
  if (parts.length !== 4) return null;
193315
193316
  const nums = parts.map((p) => Number(p));
193316
193317
  if (!nums.every((n) => Number.isFinite(n))) return null;
193317
- const clamp = (x2) => Math.max(0, Math.min(20, Math.floor(x2 * 1e3) / 1e3));
193318
- const [up, right, down, left] = nums.map((n) => clamp(Number(n)));
193318
+ const clamp2 = (x2) => Math.max(0, Math.min(20, Math.floor(x2 * 1e3) / 1e3));
193319
+ const [up, right, down, left] = nums.map((n) => clamp2(Number(n)));
193319
193320
  return { up, right, down, left };
193320
193321
  }
193321
193322
  function slidesTextShiftToDxDy(shift) {
@@ -193404,7 +193405,7 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
193404
193405
  if (!s1Bullets) return { ok: false, error: "DECK_TITLE_SLIDE_BULLETS_INVALID" };
193405
193406
  if (s1Bullets.length !== 0) return { ok: false, error: "DECK_TITLE_SLIDE_BULLETS_NOT_EMPTY" };
193406
193407
  }
193407
- const clamp = (s2, maxLen) => {
193408
+ const clamp2 = (s2, maxLen) => {
193408
193409
  const t2 = String(s2 || "").trim();
193409
193410
  if (t2.length <= maxLen) return t2;
193410
193411
  return t2.slice(0, Math.max(0, maxLen - 1)).trimEnd() + "\u2026";
@@ -193418,12 +193419,12 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
193418
193419
  for (const s2 of slides) {
193419
193420
  if (!isRecord(s2)) return { ok: false, error: "DECK_SLIDE_NOT_OBJECT" };
193420
193421
  const st0 = typeof s2.title === "string" ? s2.title.trim() : "";
193421
- const st = clamp(st0, 48);
193422
+ const st = clamp2(st0, 48);
193422
193423
  if (!st) return { ok: false, error: "DECK_SLIDE_TITLE_EMPTY" };
193423
193424
  if (hasTerminalPunct(st)) return { ok: false, error: "DECK_SLIDE_TITLE_SENTENCE_LIKE_FORBIDDEN" };
193424
193425
  s2.title = st;
193425
193426
  const sub0 = typeof s2.subtitle === "string" ? String(s2.subtitle).trim() : "";
193426
- const sub = sub0 ? clamp(sub0, 80) : "";
193427
+ const sub = sub0 ? clamp2(sub0, 80) : "";
193427
193428
  const kind0 = typeof s2.kind === "string" ? String(s2.kind).trim().toLowerCase() : "";
193428
193429
  const kind = kind0 === "title" || kind0 === "agenda" || kind0 === "section" || kind0 === "content" || kind0 === "appendix" ? kind0 : "";
193429
193430
  if (kind) s2.kind = kind;
@@ -193459,7 +193460,7 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
193459
193460
  const name = srec && typeof srec.name === "string" ? String(srec.name).trim().slice(0, 40) : "";
193460
193461
  const values = srec && Array.isArray(srec.values) ? srec.values.map((v) => typeof v === "number" && Number.isFinite(v) ? v : typeof v === "string" ? Number(v) : NaN).filter((v) => Number.isFinite(v)).slice(0, 6) : [];
193461
193462
  const caption0 = typeof c.caption === "string" ? String(c.caption).trim() : "";
193462
- const caption = caption0 ? clamp(caption0, 140) : "";
193463
+ const caption = caption0 ? clamp2(caption0, 140) : "";
193463
193464
  const okChart = type2 === "bar" && categories.length >= 2 && values.length === categories.length;
193464
193465
  if (!okChart) {
193465
193466
  s2.chart = void 0;
@@ -193483,7 +193484,7 @@ function validateDeckScriptV1(raw, expectedSlides, opts) {
193483
193484
  const headers = Array.isArray(headersRaw) ? headersRaw.map((x2) => typeof x2 === "string" ? x2.trim() : "").filter(Boolean) : [];
193484
193485
  const rows = Array.isArray(rowsRaw) ? rowsRaw.filter((r2) => Array.isArray(r2)).map((r2) => r2.map((c) => typeof c === "string" ? c.trim() : "").map((c) => c.slice(0, 80))) : [];
193485
193486
  const caption0 = typeof t0.caption === "string" ? String(t0.caption).trim() : "";
193486
- const caption = caption0 ? clamp(caption0, 140) : "";
193487
+ const caption = caption0 ? clamp2(caption0, 140) : "";
193487
193488
  const isEmptyTable = headers.length === 0 && rows.length === 0 && !caption;
193488
193489
  if (isEmptyTable) {
193489
193490
  s2.table = void 0;
@@ -193623,10 +193624,10 @@ function pickFigureCanvas(params) {
193623
193624
  const hint = typeof params.hint === "string" ? params.hint.trim() : "";
193624
193625
  const baseW = params.slideCanvas.width;
193625
193626
  const baseH = params.slideCanvas.height;
193626
- const clamp = (x2) => Math.max(512, Math.min(2048, Math.floor(x2)));
193627
- if (hint === "tall") return { width: clamp(baseW * 0.38), height: clamp(baseH * 0.72) };
193628
- if (hint === "square") return { width: clamp(baseW * 0.5), height: clamp(baseH * 0.5) };
193629
- return { width: clamp(baseW * 0.62), height: clamp(baseH * 0.42) };
193627
+ const clamp2 = (x2) => Math.max(512, Math.min(2048, Math.floor(x2)));
193628
+ if (hint === "tall") return { width: clamp2(baseW * 0.38), height: clamp2(baseH * 0.72) };
193629
+ if (hint === "square") return { width: clamp2(baseW * 0.5), height: clamp2(baseH * 0.5) };
193630
+ return { width: clamp2(baseW * 0.62), height: clamp2(baseH * 0.42) };
193630
193631
  }
193631
193632
  async function saveImageFromApiResult(params) {
193632
193633
  return await saveImageFromApiResultShared({
@@ -194534,7 +194535,7 @@ ${prompt}` : prompt,
194534
194535
  const replace = has2("replace") || has2("overwrite");
194535
194536
  const newSeries = has2("new-series") || has2("newSeries") || Boolean(envInputs.newSeries);
194536
194537
  const seriesRef = String(envInputs.series ?? pickOpt4("series") ?? "").trim();
194537
- const outDirRel = String(envInputs.outDir ?? pickOpt4("out") ?? "artifacts/novel").trim() || "artifacts/novel";
194538
+ const outDirRel = String(envInputs.outDir ?? pickOpt4("out") ?? ".maria/desktop/novel").trim() || ".maria/desktop/novel";
194538
194539
  const lang = normalizeLang(String(envInputs.lang ?? pickOpt4("lang") ?? "en"));
194539
194540
  const format2 = normalizeFormat4(String(envInputs.format ?? pickOpt4("format") ?? "md"));
194540
194541
  const genre = String(envInputs.genre ?? pickOpt4("genre") ?? "").trim() || void 0;
@@ -227615,8 +227616,8 @@ ${this.help.usage}` };
227615
227616
  const format2 = normalizeFormat5(ctx.parsed.options.format);
227616
227617
  const model = ctx.parsed.options.model ? String(ctx.parsed.options.model).trim() : void 0;
227617
227618
  const seed = ctx.parsed.options.seed ? parseSeed4(ctx.parsed.options.seed) : void 0;
227618
- const outDirRel = normalizeMangaOutDir(ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : "manga");
227619
- const outRootAbs = path87__namespace.resolve(ctx.cwd, outDirRel || "manga");
227619
+ const outDirRel = normalizeMangaOutDir(ctx.parsed.options.out ? String(ctx.parsed.options.out).trim() : ".maria/desktop/manga");
227620
+ const outRootAbs = path87__namespace.resolve(ctx.cwd, outDirRel || ".maria/desktop/manga");
227620
227621
  const stamp = nowStamp5();
227621
227622
  const inBase = inParts.length === 1 ? path87__namespace.basename(inParts[0]) : inParts.length > 1 ? "sources" : "";
227622
227623
  const folder = `${stamp}_${safeSlug3((theme || inBase || "source").slice(0, 64))}`;
@@ -228543,7 +228544,7 @@ function createSlidesField() {
228543
228544
  const format2 = normalizeFormat3(ctx.parsed.options.format);
228544
228545
  const model = typeof ctx.parsed.options.model === "string" ? String(ctx.parsed.options.model).trim() : void 0;
228545
228546
  const seed = typeof ctx.parsed.options.seed === "string" ? parseSeed3(ctx.parsed.options.seed) : void 0;
228546
- const outBaseRel = normalizeSlidesOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "slides");
228547
+ const outBaseRel = normalizeSlidesOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/slides");
228547
228548
  const wantsPdf = hasLiteFlag(ctx.parsed, "pdf");
228548
228549
  const wantsPptx = hasLiteFlag(ctx.parsed, "pptx");
228549
228550
  const titleTitleShiftRaw = typeof ctx.parsed.options["title-title-shift"] === "string" ? String(ctx.parsed.options["title-title-shift"]).trim() : "";
@@ -230075,7 +230076,7 @@ function createFilmField() {
230075
230076
  const model = modelRaw || "sora-2";
230076
230077
  const provider = typeof ctx.parsed.options.provider === "string" ? String(ctx.parsed.options.provider).trim() : void 0;
230077
230078
  const seed = ctx.parsed.options.seed ? parseSeed5(ctx.parsed.options.seed) : void 0;
230078
- const outBaseRel = normalizeFilmOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "film");
230079
+ const outBaseRel = normalizeFilmOutDir(typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/film");
230079
230080
  const res = typeof ctx.parsed.options.res === "string" && String(ctx.parsed.options.res).trim() ? String(ctx.parsed.options.res).trim() : pickDefaultResForAspect({ aspect });
230080
230081
  const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
230081
230082
  const applyExplicit = hasLiteFlag(ctx.parsed, "apply");
@@ -231001,7 +231002,7 @@ function createDocsField() {
231001
231002
  const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
231002
231003
  const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
231003
231004
  const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
231004
- const outBaseRel = (outBase || "docs").replace(/\\/g, "/");
231005
+ const outBaseRel = (outBase || ".maria/desktop/docs").replace(/\\/g, "/");
231005
231006
  const src = await resolveInputSourceText(ctx);
231006
231007
  if (!src.sourceText.trim()) return { text: `Usage:
231007
231008
  ${this.help.usage}` };
@@ -231308,8 +231309,8 @@ async function loadCommonSettings(cwd) {
231308
231309
  if (!r2) return null;
231309
231310
  const nums = ["up", "right", "down", "left"].map((k) => Number(r2[k]));
231310
231311
  if (!nums.every((n) => Number.isFinite(n))) return null;
231311
- const clamp = (x2) => Math.max(0, Math.min(20, Math.floor(x2 * 1e3) / 1e3));
231312
- return { up: clamp(nums[0]), right: clamp(nums[1]), down: clamp(nums[2]), left: clamp(nums[3]) };
231312
+ const clamp2 = (x2) => Math.max(0, Math.min(20, Math.floor(x2 * 1e3) / 1e3));
231313
+ return { up: clamp2(nums[0]), right: clamp2(nums[1]), down: clamp2(nums[2]), left: clamp2(nums[3]) };
231313
231314
  };
231314
231315
  const titleTitleShift = normShift(v.titleTitleShift);
231315
231316
  const titleSubtitleShift = normShift(v.titleSubtitleShift);
@@ -256282,6 +256283,1529 @@ var init_phone_routes = __esm({
256282
256283
  init_desktop_server_helpers();
256283
256284
  }
256284
256285
  });
256286
+
256287
+ // services/google/admin-resources.ts
256288
+ async function adminFetch(token, url, init) {
256289
+ const headers = {
256290
+ Authorization: `Bearer ${token}`,
256291
+ ...init?.headers || {}
256292
+ };
256293
+ return fetch(url, { ...init, headers });
256294
+ }
256295
+ function slugify2(name, prefix) {
256296
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `${prefix}-${Date.now()}`;
256297
+ }
256298
+ function parseFloorNames(raw) {
256299
+ const trimmed = raw.trim();
256300
+ if (!trimmed) return [];
256301
+ const n = Number(trimmed);
256302
+ if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
256303
+ return Array.from({ length: n }, (_, i2) => String(i2 + 1));
256304
+ }
256305
+ return trimmed.split(",").map((f3) => f3.trim()).filter(Boolean);
256306
+ }
256307
+ function toBuilding(raw) {
256308
+ return {
256309
+ buildingId: String(raw.buildingId || ""),
256310
+ buildingName: String(raw.buildingName || ""),
256311
+ description: String(raw.description || ""),
256312
+ floorNames: Array.isArray(raw.floorNames) ? raw.floorNames.map(String) : []
256313
+ };
256314
+ }
256315
+ function toResource(raw) {
256316
+ return {
256317
+ resourceId: String(raw.resourceId || ""),
256318
+ resourceEmail: String(raw.resourceEmail || ""),
256319
+ resourceName: String(raw.resourceName || ""),
256320
+ resourceType: String(raw.resourceType || ""),
256321
+ capacity: Number(raw.capacity || 0),
256322
+ buildingId: String(raw.buildingId || ""),
256323
+ floorName: String(raw.floorName || ""),
256324
+ userVisibleDescription: String(raw.userVisibleDescription || ""),
256325
+ resourceDescription: String(raw.resourceDescription || ""),
256326
+ generatedResourceName: String(raw.generatedResourceName || "")
256327
+ };
256328
+ }
256329
+ async function parseErrorText(res) {
256330
+ return res.text().catch(() => "");
256331
+ }
256332
+ function apiError(status, errText) {
256333
+ if (status === 403) return { ok: false, error: "permission_denied", status };
256334
+ if (status === 404) return { ok: false, error: "not_found", status };
256335
+ return { ok: false, error: `Admin SDK: ${status} ${errText.slice(0, 200)}`, status };
256336
+ }
256337
+ async function listBuildingsRaw(token, signal) {
256338
+ const res = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/buildings?maxResults=500`, signal ? { signal } : void 0);
256339
+ if (!res.ok) return apiError(res.status, await parseErrorText(res));
256340
+ const data = await res.json();
256341
+ const items = Array.isArray(data.buildings) ? data.buildings : [];
256342
+ return { ok: true, data: items.map(toBuilding) };
256343
+ }
256344
+ async function createBuildingRaw(token, params, signal) {
256345
+ const buildingId = params.buildingId || slugify2(params.name, "bld");
256346
+ const body = { buildingId, buildingName: params.name };
256347
+ if (params.description) body.description = params.description;
256348
+ if (params.floorNames) {
256349
+ body.floorNames = params.floorNames;
256350
+ } else if (params.floors) {
256351
+ body.floorNames = parseFloorNames(params.floors);
256352
+ }
256353
+ const res = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/buildings`, {
256354
+ method: "POST",
256355
+ headers: { "Content-Type": "application/json" },
256356
+ body: JSON.stringify(body),
256357
+ ...signal ? { signal } : {}
256358
+ });
256359
+ if (!res.ok) return apiError(res.status, await parseErrorText(res));
256360
+ return { ok: true, data: toBuilding(await res.json()) };
256361
+ }
256362
+ async function updateBuildingRaw(token, buildingId, body, signal) {
256363
+ const res = await adminFetch(
256364
+ token,
256365
+ `${ADMIN_RESOURCES_BASE}/buildings/${encodeURIComponent(buildingId)}`,
256366
+ { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), ...signal ? { signal } : {} }
256367
+ );
256368
+ if (!res.ok) return apiError(res.status, await parseErrorText(res));
256369
+ return { ok: true, data: toBuilding(await res.json()) };
256370
+ }
256371
+ async function listResourcesRaw(token, opts, signal) {
256372
+ let url = `${ADMIN_RESOURCES_BASE}/calendars?maxResults=500`;
256373
+ if (opts?.buildingId) {
256374
+ url += `&query=buildingId="${encodeURIComponent(opts.buildingId)}"`;
256375
+ }
256376
+ const res = await adminFetch(token, url, signal ? { signal } : void 0);
256377
+ if (!res.ok) return apiError(res.status, await parseErrorText(res));
256378
+ const data = await res.json();
256379
+ let items = Array.isArray(data.items) ? data.items : [];
256380
+ if (opts?.capacity && opts.capacity > 0) {
256381
+ items = items.filter((r2) => Number(r2.capacity || 0) >= opts.capacity);
256382
+ }
256383
+ return { ok: true, data: items.map(toResource) };
256384
+ }
256385
+ async function getResourceRaw(token, resourceId, signal) {
256386
+ const res = await adminFetch(
256387
+ token,
256388
+ `${ADMIN_RESOURCES_BASE}/calendars/${encodeURIComponent(resourceId)}`,
256389
+ signal ? { signal } : void 0
256390
+ );
256391
+ if (!res.ok) return apiError(res.status, await parseErrorText(res));
256392
+ return { ok: true, data: toResource(await res.json()) };
256393
+ }
256394
+ async function createResourceRaw(token, params, signal) {
256395
+ const body = {
256396
+ resourceName: params.name,
256397
+ resourceType: params.resourceType || "CONFERENCE_ROOM"
256398
+ };
256399
+ if (params.resourceId) body.resourceId = params.resourceId;
256400
+ if (params.buildingId) body.buildingId = params.buildingId;
256401
+ if (params.capacity) body.capacity = params.capacity;
256402
+ if (params.floorName) body.floorName = params.floorName;
256403
+ if (params.description) body.resourceDescription = params.description;
256404
+ const res = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/calendars`, {
256405
+ method: "POST",
256406
+ headers: { "Content-Type": "application/json" },
256407
+ body: JSON.stringify(body),
256408
+ ...signal ? { signal } : {}
256409
+ });
256410
+ if (!res.ok) return apiError(res.status, await parseErrorText(res));
256411
+ return { ok: true, data: toResource(await res.json()) };
256412
+ }
256413
+ async function updateResourceRaw(token, resourceId, body, signal) {
256414
+ const res = await adminFetch(
256415
+ token,
256416
+ `${ADMIN_RESOURCES_BASE}/calendars/${encodeURIComponent(resourceId)}`,
256417
+ { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), ...signal ? { signal } : {} }
256418
+ );
256419
+ if (!res.ok) return apiError(res.status, await parseErrorText(res));
256420
+ return { ok: true, data: toResource(await res.json()) };
256421
+ }
256422
+ async function fetchExistingBuildingIds(token, signal) {
256423
+ const ids = /* @__PURE__ */ new Set();
256424
+ try {
256425
+ const result = await listBuildingsRaw(token, signal);
256426
+ if (result.ok && result.data) {
256427
+ for (const b of result.data) {
256428
+ if (b.buildingId) ids.add(b.buildingId);
256429
+ }
256430
+ }
256431
+ } catch {
256432
+ }
256433
+ return ids;
256434
+ }
256435
+ var ADMIN_RESOURCES_BASE;
256436
+ var init_admin_resources = __esm({
256437
+ "services/google/admin-resources.ts"() {
256438
+ ADMIN_RESOURCES_BASE = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources";
256439
+ }
256440
+ });
256441
+
256442
+ // services/desktop/routes/calendar-api-routes.ts
256443
+ async function getToken2() {
256444
+ const mgr = new GoogleOAuthManager();
256445
+ return mgr.getValidToken();
256446
+ }
256447
+ function notConnected2(res) {
256448
+ respondJson2(res, 200, { ok: false, error: "not_connected" });
256449
+ return true;
256450
+ }
256451
+ function resolveProjectId2() {
256452
+ return process.env.GCLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT || process.env.FIREBASE_PROJECT_ID || process.env.GCP_PROJECT_ID || "";
256453
+ }
256454
+ async function getFirestore2() {
256455
+ if (_firestoreInstance2) return _firestoreInstance2;
256456
+ if (_firestoreInitPromise2) return _firestoreInitPromise2;
256457
+ _firestoreInitPromise2 = (async () => {
256458
+ const projectId = resolveProjectId2();
256459
+ if (!projectId) return null;
256460
+ try {
256461
+ const { Firestore: Firestore3 } = await Promise.resolve().then(() => __toESM(require_src21(), 1));
256462
+ _firestoreInstance2 = new Firestore3({ projectId });
256463
+ return _firestoreInstance2;
256464
+ } catch {
256465
+ return null;
256466
+ }
256467
+ })();
256468
+ return _firestoreInitPromise2;
256469
+ }
256470
+ function firestoreUnavailable(res) {
256471
+ respondJson2(res, 503, {
256472
+ ok: false,
256473
+ error: "firestore_unavailable",
256474
+ message: "Firestore is not configured. Set GCLOUD_PROJECT or GOOGLE_CLOUD_PROJECT env var."
256475
+ });
256476
+ return true;
256477
+ }
256478
+ function generateReservationId() {
256479
+ const now = /* @__PURE__ */ new Date();
256480
+ const datePart = [
256481
+ now.getFullYear(),
256482
+ String(now.getMonth() + 1).padStart(2, "0"),
256483
+ String(now.getDate()).padStart(2, "0")
256484
+ ].join("");
256485
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
256486
+ let random = "";
256487
+ for (let i2 = 0; i2 < 5; i2++) {
256488
+ random += chars[Math.floor(Math.random() * chars.length)];
256489
+ }
256490
+ return `R-${datePart}-${random}`;
256491
+ }
256492
+ function buildRrule(type2, count, until) {
256493
+ const freqMap = {
256494
+ daily: "DAILY",
256495
+ weekly: "WEEKLY",
256496
+ biweekly: "WEEKLY",
256497
+ monthly: "MONTHLY"
256498
+ };
256499
+ const freq = freqMap[type2];
256500
+ if (!freq) return null;
256501
+ let rule = `RRULE:FREQ=${freq}`;
256502
+ if (type2 === "biweekly") rule += ";INTERVAL=2";
256503
+ if (count) rule += `;COUNT=${count}`;
256504
+ else if (until) rule += `;UNTIL=${until.replace(/-/g, "")}T235959Z`;
256505
+ return rule;
256506
+ }
256507
+ async function fetchResources(token, buildingId, minCapacity) {
256508
+ const result = await listResourcesRaw(token, { buildingId, capacity: minCapacity });
256509
+ if (!result.ok || !result.data) return [];
256510
+ return result.data;
256511
+ }
256512
+ function findSlots(params) {
256513
+ const {
256514
+ busyMap,
256515
+ required,
256516
+ optional,
256517
+ resourceEmails,
256518
+ durationMs,
256519
+ dateFrom,
256520
+ dateTo,
256521
+ workStartHour,
256522
+ workEndHour,
256523
+ offset,
256524
+ bufferMinutes = DEFAULT_BUFFER_MINUTES,
256525
+ timeZone = DEFAULT_TIMEZONE
256526
+ } = params;
256527
+ const minBufferMs = bufferMinutes * 60 * 1e3;
256528
+ const allTargets = [...resourceEmails, ...required, ...optional];
256529
+ const candidates = [];
256530
+ const [fromY, fromM, fromD] = dateFrom.split("-").map(Number);
256531
+ const [toY, toM, toD] = dateTo.split("-").map(Number);
256532
+ const dStart = new Date(Date.UTC(fromY, fromM - 1, fromD));
256533
+ const dEnd = new Date(Date.UTC(toY, toM - 1, toD));
256534
+ for (let d = new Date(dStart); d <= dEnd; d.setUTCDate(d.getUTCDate() + 1)) {
256535
+ const yy = d.getUTCFullYear();
256536
+ const mm = d.getUTCMonth() + 1;
256537
+ const dd = d.getUTCDate();
256538
+ const dayStr = `${yy}-${String(mm).padStart(2, "0")}-${String(dd).padStart(2, "0")}`;
256539
+ const weekdayRef = new Date(Date.UTC(yy, mm - 1, dd, 12, 0, 0));
256540
+ if (weekdayRef.getUTCDay() === 0 || weekdayRef.getUTCDay() === 6) continue;
256541
+ const slotDayStart = makeDateInTz(dayStr, workStartHour, 0, timeZone);
256542
+ const slotDayEnd = makeDateInTz(dayStr, workEndHour, 0, timeZone);
256543
+ for (let t2 = slotDayStart.getTime(); t2 + durationMs <= slotDayEnd.getTime(); t2 += SLOT_STEP_MS) {
256544
+ const slotS = t2;
256545
+ const slotE = t2 + durationMs;
256546
+ let requiredOk = true;
256547
+ let resourcesOk = true;
256548
+ let availCount = 0;
256549
+ const missingOpt = [];
256550
+ const availableResourceEmails = [];
256551
+ for (const email of resourceEmails) {
256552
+ const busy = busyMap[email] || [];
256553
+ const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
256554
+ if (isBusy) {
256555
+ resourcesOk = false;
256556
+ } else {
256557
+ availableResourceEmails.push(email);
256558
+ availCount++;
256559
+ }
256560
+ }
256561
+ if (resourceEmails.length > 0 && !resourcesOk && availableResourceEmails.length === 0) continue;
256562
+ for (const email of required) {
256563
+ const busy = busyMap[email] || [];
256564
+ const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
256565
+ if (isBusy) {
256566
+ requiredOk = false;
256567
+ } else {
256568
+ availCount++;
256569
+ }
256570
+ }
256571
+ if (!requiredOk) continue;
256572
+ for (const email of optional) {
256573
+ const busy = busyMap[email] || [];
256574
+ const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
256575
+ if (isBusy) {
256576
+ missingOpt.push(email);
256577
+ } else {
256578
+ availCount++;
256579
+ }
256580
+ }
256581
+ const mustBeAvailable = [...resourceEmails.filter((e2) => availableResourceEmails.includes(e2)), ...required];
256582
+ let bufBefore = Infinity;
256583
+ let bufAfter = Infinity;
256584
+ for (const email of mustBeAvailable) {
256585
+ const busy = busyMap[email] || [];
256586
+ let gapBefore = slotS - slotDayStart.getTime();
256587
+ let gapAfter = slotDayEnd.getTime() - slotE;
256588
+ for (const b of busy) {
256589
+ if (b.e <= slotS) gapBefore = Math.min(gapBefore, slotS - b.e);
256590
+ if (b.s >= slotE) gapAfter = Math.min(gapAfter, b.s - slotE);
256591
+ }
256592
+ bufBefore = Math.min(bufBefore, gapBefore);
256593
+ bufAfter = Math.min(bufAfter, gapAfter);
256594
+ }
256595
+ if (!isFinite(bufBefore)) bufBefore = 12 * 60 * 60 * 1e3;
256596
+ if (!isFinite(bufAfter)) bufAfter = 12 * 60 * 60 * 1e3;
256597
+ candidates.push({
256598
+ start: slotS,
256599
+ end: slotE,
256600
+ allAvailable: availCount === allTargets.length,
256601
+ availableCount: availCount,
256602
+ totalParticipants: allTargets.length,
256603
+ missingOptional: missingOpt,
256604
+ bufferBefore: bufBefore,
256605
+ bufferAfter: bufAfter,
256606
+ resourceEmails: availableResourceEmails
256607
+ });
256608
+ }
256609
+ }
256610
+ const hasSufficientBuffer = (c) => c.bufferBefore >= minBufferMs && c.bufferAfter >= minBufferMs;
256611
+ const bufferImbalance = (c) => Math.abs(c.bufferBefore - c.bufferAfter);
256612
+ candidates.sort((a, b) => {
256613
+ if (a.allAvailable !== b.allAvailable) return a.allAvailable ? -1 : 1;
256614
+ if (a.availableCount !== b.availableCount) return b.availableCount - a.availableCount;
256615
+ if (a.missingOptional.length !== b.missingOptional.length) return a.missingOptional.length - b.missingOptional.length;
256616
+ const optIdx = (m2) => {
256617
+ let worst = -1;
256618
+ for (const e2 of m2) {
256619
+ const i2 = optional.indexOf(e2);
256620
+ if (i2 > worst) worst = i2;
256621
+ }
256622
+ return worst;
256623
+ };
256624
+ const ai = optIdx(a.missingOptional);
256625
+ const bi = optIdx(b.missingOptional);
256626
+ if (ai !== bi) return ai - bi;
256627
+ const aSuf = hasSufficientBuffer(a);
256628
+ const bSuf = hasSufficientBuffer(b);
256629
+ if (aSuf && bSuf) {
256630
+ if (a.start !== b.start) return a.start - b.start;
256631
+ return bufferImbalance(a) - bufferImbalance(b);
256632
+ }
256633
+ if (aSuf !== bSuf) return aSuf ? -1 : 1;
256634
+ const aTot = a.bufferBefore + a.bufferAfter;
256635
+ const bTot = b.bufferBefore + b.bufferAfter;
256636
+ if (aTot !== bTot) return bTot - aTot;
256637
+ return bufferImbalance(a) - bufferImbalance(b);
256638
+ });
256639
+ const nonOverlapping = [];
256640
+ const needed = (offset + 1) * TOP_SLOT_COUNT + 1;
256641
+ for (const c of candidates) {
256642
+ const overlaps = nonOverlapping.some((t2) => t2.start < c.end && t2.end > c.start);
256643
+ if (!overlaps) nonOverlapping.push(c);
256644
+ if (nonOverlapping.length >= needed) break;
256645
+ }
256646
+ const start = offset * TOP_SLOT_COUNT;
256647
+ const page = nonOverlapping.slice(start, start + TOP_SLOT_COUNT);
256648
+ return { slots: page, hasMore: nonOverlapping.length > start + TOP_SLOT_COUNT };
256649
+ }
256650
+ async function handleCalendarApiRoute(method, pathname, req, res) {
256651
+ if (!pathname.startsWith("/api/calendar/")) return false;
256652
+ if (method === "GET" && pathname === "/api/calendar/resources") {
256653
+ const token = await getToken2();
256654
+ if (!token) return notConnected2(res);
256655
+ const url = new URL(req.url || "/", "http://localhost");
256656
+ const buildingId = url.searchParams.get("buildingId") || void 0;
256657
+ const capacityRaw = url.searchParams.get("capacity");
256658
+ const capacity = capacityRaw ? Math.max(0, Number(capacityRaw) || 0) : void 0;
256659
+ try {
256660
+ const resources = await fetchResources(token, buildingId, capacity);
256661
+ return respondJson2(res, 200, { ok: true, resources }), true;
256662
+ } catch (err) {
256663
+ const msg = err instanceof Error ? err.message : String(err);
256664
+ return respondJson2(res, 200, { ok: false, error: `Admin SDK: ${msg}` }), true;
256665
+ }
256666
+ }
256667
+ if (method === "GET" && pathname === "/api/calendar/buildings") {
256668
+ const token = await getToken2();
256669
+ if (!token) return notConnected2(res);
256670
+ const result = await listBuildingsRaw(token);
256671
+ if (!result.ok) return respondJson2(res, 200, { ok: false, error: result.error }), true;
256672
+ return respondJson2(res, 200, { ok: true, buildings: result.data }), true;
256673
+ }
256674
+ if (method === "POST" && pathname === "/api/calendar/buildings") {
256675
+ const token = await getToken2();
256676
+ if (!token) return notConnected2(res);
256677
+ const reqBody = await parseJsonBody2(req);
256678
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
256679
+ const buildingName = String(reqBody.buildingName || "").trim();
256680
+ if (!buildingName) return respondJson2(res, 400, { ok: false, error: "buildingName is required" }), true;
256681
+ const buildingId = String(reqBody.buildingId || "").trim() || slugify2(buildingName, "bld");
256682
+ const floorNames = reqBody.floorNames ? Array.isArray(reqBody.floorNames) ? reqBody.floorNames.map(String) : void 0 : void 0;
256683
+ const floors = !floorNames && reqBody.floorNames ? String(reqBody.floorNames).trim() : void 0;
256684
+ const result = await createBuildingRaw(token, {
256685
+ name: buildingName,
256686
+ buildingId,
256687
+ description: reqBody.description ? String(reqBody.description).trim() : void 0,
256688
+ floorNames: floorNames || void 0,
256689
+ floors: floors || void 0
256690
+ });
256691
+ if (!result.ok) {
256692
+ if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
256693
+ return respondJson2(res, 200, { ok: false, error: result.error }), true;
256694
+ }
256695
+ return respondJson2(res, 200, { ok: true, building: result.data }), true;
256696
+ }
256697
+ if (method === "POST" && pathname === "/api/calendar/resources") {
256698
+ const token = await getToken2();
256699
+ if (!token) return notConnected2(res);
256700
+ const reqBody = await parseJsonBody2(req);
256701
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
256702
+ const resourceName = String(reqBody.resourceName || "").trim();
256703
+ if (!resourceName) return respondJson2(res, 400, { ok: false, error: "resourceName is required" }), true;
256704
+ const resourceId = String(reqBody.resourceId || "").trim() || slugify2(resourceName, "res");
256705
+ const result = await createResourceRaw(token, {
256706
+ name: resourceName,
256707
+ resourceId,
256708
+ buildingId: reqBody.buildingId ? String(reqBody.buildingId).trim() : void 0,
256709
+ capacity: reqBody.capacity ? Math.max(0, Number(reqBody.capacity) || 0) : void 0,
256710
+ resourceType: String(reqBody.resourceType || "CONFERENCE_ROOM").trim(),
256711
+ floorName: reqBody.floorName ? String(reqBody.floorName).trim() : void 0,
256712
+ description: reqBody.resourceDescription ? String(reqBody.resourceDescription).trim() : void 0
256713
+ });
256714
+ if (!result.ok) {
256715
+ if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
256716
+ return respondJson2(res, 200, { ok: false, error: result.error }), true;
256717
+ }
256718
+ return respondJson2(res, 200, { ok: true, resource: result.data }), true;
256719
+ }
256720
+ if (method === "PATCH" && pathname.startsWith("/api/calendar/buildings/")) {
256721
+ const buildingId = decodeURIComponent(pathname.slice("/api/calendar/buildings/".length));
256722
+ if (!buildingId) return respondJson2(res, 400, { ok: false, error: "buildingId is required" }), true;
256723
+ const token = await getToken2();
256724
+ if (!token) return notConnected2(res);
256725
+ const reqBody = await parseJsonBody2(req);
256726
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
256727
+ const body = {};
256728
+ if (reqBody.buildingName) body.buildingName = String(reqBody.buildingName).trim();
256729
+ if (reqBody.description !== void 0) body.description = String(reqBody.description).trim();
256730
+ if (reqBody.floorNames) {
256731
+ if (Array.isArray(reqBody.floorNames)) {
256732
+ body.floorNames = reqBody.floorNames.map(String);
256733
+ } else {
256734
+ const raw = String(reqBody.floorNames).trim();
256735
+ const n = Number(raw);
256736
+ if (!isNaN(n) && n > 0 && Number.isInteger(n)) {
256737
+ body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
256738
+ } else {
256739
+ body.floorNames = raw.split(",").map((f3) => f3.trim()).filter(Boolean);
256740
+ }
256741
+ }
256742
+ }
256743
+ if (Object.keys(body).length === 0) {
256744
+ return respondJson2(res, 400, { ok: false, error: "No fields to update" }), true;
256745
+ }
256746
+ const result = await updateBuildingRaw(token, buildingId, body);
256747
+ if (!result.ok) {
256748
+ if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
256749
+ if (result.status === 404) return respondJson2(res, 404, { ok: false, error: "Building not found" }), true;
256750
+ return respondJson2(res, 200, { ok: false, error: result.error }), true;
256751
+ }
256752
+ return respondJson2(res, 200, { ok: true, building: result.data }), true;
256753
+ }
256754
+ if (method === "PATCH" && pathname.startsWith("/api/calendar/resources/")) {
256755
+ const resourceId = decodeURIComponent(pathname.slice("/api/calendar/resources/".length));
256756
+ if (!resourceId) return respondJson2(res, 400, { ok: false, error: "resourceId is required" }), true;
256757
+ const token = await getToken2();
256758
+ if (!token) return notConnected2(res);
256759
+ const reqBody = await parseJsonBody2(req);
256760
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
256761
+ const body = {};
256762
+ if (reqBody.resourceName) body.resourceName = String(reqBody.resourceName).trim();
256763
+ if (reqBody.buildingId !== void 0) body.buildingId = String(reqBody.buildingId).trim();
256764
+ if (reqBody.capacity !== void 0) body.capacity = Math.max(0, Number(reqBody.capacity) || 0);
256765
+ if (reqBody.resourceType) body.resourceType = String(reqBody.resourceType).trim();
256766
+ if (reqBody.floorName !== void 0) body.floorName = String(reqBody.floorName).trim();
256767
+ if (reqBody.resourceDescription !== void 0) body.resourceDescription = String(reqBody.resourceDescription).trim();
256768
+ if (Object.keys(body).length === 0) {
256769
+ return respondJson2(res, 400, { ok: false, error: "No fields to update" }), true;
256770
+ }
256771
+ const result = await updateResourceRaw(token, resourceId, body);
256772
+ if (!result.ok) {
256773
+ if (result.status === 403) return respondJson2(res, 403, { ok: false, error: "permission_denied", message: "Google Workspace admin privileges required" }), true;
256774
+ if (result.status === 404) return respondJson2(res, 404, { ok: false, error: "Resource not found" }), true;
256775
+ return respondJson2(res, 200, { ok: false, error: result.error }), true;
256776
+ }
256777
+ return respondJson2(res, 200, { ok: true, resource: result.data }), true;
256778
+ }
256779
+ if (method === "POST" && pathname === "/api/calendar/slots") {
256780
+ const token = await getToken2();
256781
+ if (!token) return notConnected2(res);
256782
+ const reqBody = await parseJsonBody2(req);
256783
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
256784
+ const resourceId = String(reqBody.resourceId || "").trim();
256785
+ const buildingId = String(reqBody.buildingId || "").trim();
256786
+ const capacity = Number(reqBody.capacity || 0) || 0;
256787
+ const dateSingle = String(reqBody.date || "").trim();
256788
+ const dateFrom = dateSingle || String(reqBody.dateFrom || "").trim();
256789
+ const dateTo = dateSingle || String(reqBody.dateTo || "").trim();
256790
+ const durationMinutes = Math.max(15, Math.min(480, Number(reqBody.durationMinutes || DEFAULT_DURATION_MINUTES) || DEFAULT_DURATION_MINUTES));
256791
+ const workStartHour = Number(reqBody.workStartHour ?? DEFAULT_WORK_START_HOUR);
256792
+ const workEndHour = Number(reqBody.workEndHour ?? DEFAULT_WORK_END_HOUR);
256793
+ const offset = Math.max(0, Number(reqBody.offset || 0) || 0);
256794
+ const bufferMinutes = Math.max(0, Math.min(60, Number(reqBody.bufferMinutes ?? DEFAULT_BUFFER_MINUTES) || 0));
256795
+ const required = Array.isArray(reqBody.required) ? reqBody.required.map(String).filter(Boolean) : [];
256796
+ const optional = Array.isArray(reqBody.optional) ? reqBody.optional.map(String).filter(Boolean) : [];
256797
+ if (!dateFrom || !dateTo) {
256798
+ return respondJson2(res, 400, { ok: false, error: "date or dateFrom/dateTo is required (YYYY-MM-DD)" }), true;
256799
+ }
256800
+ let resourceEmails = [];
256801
+ let resolvedResources = [];
256802
+ if (resourceId) {
256803
+ resourceEmails = [resourceId];
256804
+ } else if (buildingId || capacity > 0) {
256805
+ resolvedResources = await fetchResources(
256806
+ token,
256807
+ buildingId || void 0,
256808
+ capacity > 0 ? capacity : void 0
256809
+ );
256810
+ resourceEmails = resolvedResources.map((r2) => r2.resourceEmail).filter(Boolean);
256811
+ } else if (required.length === 0 && optional.length === 0) {
256812
+ resolvedResources = await fetchResources(token);
256813
+ resourceEmails = resolvedResources.map((r2) => r2.resourceEmail).filter(Boolean);
256814
+ }
256815
+ const allTargets = [...resourceEmails, ...required, ...optional];
256816
+ if (allTargets.length === 0) {
256817
+ return respondJson2(res, 400, { ok: false, error: "At least one target is required (resourceId, buildingId, required, or optional)" }), true;
256818
+ }
256819
+ const tz = String(reqBody.timeZone || "").trim() || DEFAULT_TIMEZONE;
256820
+ const timeMin = makeDateInTz(dateFrom, 0, 0, tz).toISOString();
256821
+ const timeMax = makeDateInTz(dateTo, 23, 59, tz).toISOString();
256822
+ const fbBody = {
256823
+ timeMin,
256824
+ timeMax,
256825
+ timeZone: tz,
256826
+ items: allTargets.map((e2) => ({ id: e2 }))
256827
+ };
256828
+ const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
256829
+ method: "POST",
256830
+ headers: { "Content-Type": "application/json" },
256831
+ body: JSON.stringify(fbBody)
256832
+ });
256833
+ if (!fbRes.ok) {
256834
+ const err = await fbRes.text().catch(() => "");
256835
+ return respondJson2(res, 200, { ok: false, error: `FreeBusy API: ${fbRes.status} ${err}` }), true;
256836
+ }
256837
+ const fbData = await fbRes.json();
256838
+ const calendars = fbData.calendars || {};
256839
+ const busyMap = {};
256840
+ for (const email of allTargets) {
256841
+ const cal = calendars[email];
256842
+ busyMap[email] = Array.isArray(cal?.busy) ? cal.busy.map((b) => ({ s: new Date(b.start).getTime(), e: new Date(b.end).getTime() })) : [];
256843
+ }
256844
+ const durationMs = durationMinutes * 60 * 1e3;
256845
+ const { slots: top, hasMore } = findSlots({
256846
+ busyMap,
256847
+ required,
256848
+ optional,
256849
+ resourceEmails,
256850
+ durationMs,
256851
+ dateFrom,
256852
+ dateTo,
256853
+ workStartHour,
256854
+ workEndHour,
256855
+ offset,
256856
+ bufferMinutes,
256857
+ timeZone: tz
256858
+ });
256859
+ const slots = top.map((c, i2) => ({
256860
+ rank: i2 + 1,
256861
+ start: new Date(c.start).toISOString(),
256862
+ end: new Date(c.end).toISOString(),
256863
+ durationMinutes,
256864
+ allAvailable: c.allAvailable,
256865
+ availableCount: c.availableCount,
256866
+ totalParticipants: c.totalParticipants,
256867
+ missingOptional: c.missingOptional,
256868
+ bufferBeforeMinutes: Math.round(c.bufferBefore / 6e4),
256869
+ bufferAfterMinutes: Math.round(c.bufferAfter / 6e4),
256870
+ resourceEmails: c.resourceEmails
256871
+ }));
256872
+ const resourceInfo = resolvedResources.length > 0 ? resolvedResources.map((r2) => ({
256873
+ resourceEmail: r2.resourceEmail,
256874
+ resourceName: r2.resourceName,
256875
+ capacity: r2.capacity
256876
+ })) : void 0;
256877
+ return respondJson2(res, 200, {
256878
+ ok: true,
256879
+ slots,
256880
+ hasMore,
256881
+ ...resourceInfo ? { resources: resourceInfo } : {},
256882
+ durationMinutes
256883
+ }), true;
256884
+ }
256885
+ if (method === "POST" && pathname === "/api/calendar/check") {
256886
+ const token = await getToken2();
256887
+ if (!token) return notConnected2(res);
256888
+ const reqBody = await parseJsonBody2(req);
256889
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
256890
+ const targets = Array.isArray(reqBody.targets) ? reqBody.targets.map(String).filter(Boolean) : [];
256891
+ const datetime = String(reqBody.datetime || "").trim();
256892
+ const durationMinutes = Math.max(15, Math.min(480, Number(reqBody.durationMinutes || DEFAULT_DURATION_MINUTES) || DEFAULT_DURATION_MINUTES));
256893
+ if (!targets.length) return respondJson2(res, 400, { ok: false, error: "targets array is required" }), true;
256894
+ if (!datetime) return respondJson2(res, 400, { ok: false, error: "datetime is required (ISO 8601)" }), true;
256895
+ const startDate = new Date(datetime);
256896
+ const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1e3);
256897
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
256898
+ const fbBody = {
256899
+ timeMin: startDate.toISOString(),
256900
+ timeMax: endDate.toISOString(),
256901
+ timeZone: tz,
256902
+ items: targets.map((e2) => ({ id: e2 }))
256903
+ };
256904
+ const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
256905
+ method: "POST",
256906
+ headers: { "Content-Type": "application/json" },
256907
+ body: JSON.stringify(fbBody)
256908
+ });
256909
+ if (!fbRes.ok) {
256910
+ const err = await fbRes.text().catch(() => "");
256911
+ return respondJson2(res, 200, { ok: false, error: `FreeBusy API: ${fbRes.status} ${err}` }), true;
256912
+ }
256913
+ const fbData = await fbRes.json();
256914
+ const calendars = fbData.calendars || {};
256915
+ let allAvailable = true;
256916
+ const statuses = targets.map((id) => {
256917
+ const cal = calendars[id];
256918
+ const conflicts = Array.isArray(cal?.busy) ? cal.busy.map((b) => ({ start: b.start, end: b.end })) : [];
256919
+ const available = conflicts.length === 0;
256920
+ if (!available) allAvailable = false;
256921
+ return { id, available, conflicts };
256922
+ });
256923
+ return respondJson2(res, 200, {
256924
+ ok: true,
256925
+ allAvailable,
256926
+ datetime: startDate.toISOString(),
256927
+ durationMinutes,
256928
+ statuses
256929
+ }), true;
256930
+ }
256931
+ if (method === "POST" && pathname === "/api/calendar/reserve") {
256932
+ const token = await getToken2();
256933
+ if (!token) return notConnected2(res);
256934
+ const db = await getFirestore2();
256935
+ if (!db) return firestoreUnavailable(res);
256936
+ const reqBody = await parseJsonBody2(req);
256937
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
256938
+ const resourceEmail = String(reqBody.resourceEmail || "").trim();
256939
+ const start = String(reqBody.start || "").trim();
256940
+ const end = String(reqBody.end || "").trim();
256941
+ const title = String(reqBody.title || "").trim();
256942
+ const description = String(reqBody.description || "").trim();
256943
+ const bookerName = String(reqBody.bookerName || "").trim();
256944
+ const bookerPhone = String(reqBody.bookerPhone || "").trim();
256945
+ const bookerEmail = String(reqBody.bookerEmail || "").trim();
256946
+ const callSid = String(reqBody.callSid || "").trim();
256947
+ const bookedVia = String(reqBody.bookedVia || "phone").trim();
256948
+ const isAllDay = reqBody.isAllDay === true;
256949
+ const recurrenceTypeRaw = String(reqBody.recurrenceType || "").trim();
256950
+ const recurrenceType = VALID_RECURRENCE_TYPES.has(recurrenceTypeRaw) ? recurrenceTypeRaw : null;
256951
+ const recurrenceCount = recurrenceType && reqBody.recurrenceCount ? Math.max(1, Math.min(365, Number(reqBody.recurrenceCount) || 0)) : void 0;
256952
+ const recurrenceUntil = recurrenceType && typeof reqBody.recurrenceUntil === "string" ? reqBody.recurrenceUntil.trim() : void 0;
256953
+ if (!start) return respondJson2(res, 400, { ok: false, error: "start is required (ISO 8601)" }), true;
256954
+ if (!end) return respondJson2(res, 400, { ok: false, error: "end is required (ISO 8601)" }), true;
256955
+ if (!title) return respondJson2(res, 400, { ok: false, error: "title is required" }), true;
256956
+ const rrule = recurrenceType ? buildRrule(recurrenceType, recurrenceCount, recurrenceUntil) : null;
256957
+ const calendarId = resourceEmail || "primary";
256958
+ if (resourceEmail) {
256959
+ try {
256960
+ const fbBody = {
256961
+ timeMin: start,
256962
+ timeMax: end,
256963
+ timeZone: DEFAULT_TIMEZONE,
256964
+ items: [{ id: calendarId }]
256965
+ };
256966
+ const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
256967
+ method: "POST",
256968
+ headers: { "Content-Type": "application/json" },
256969
+ body: JSON.stringify(fbBody)
256970
+ });
256971
+ if (fbRes.ok) {
256972
+ const fbData = await fbRes.json();
256973
+ const fbCalendars = fbData.calendars || {};
256974
+ const busyPeriods = fbCalendars[calendarId]?.busy || [];
256975
+ if (busyPeriods.length > 0) {
256976
+ return respondJson2(res, 409, {
256977
+ ok: false,
256978
+ error: "slot_conflict",
256979
+ message: "The requested time slot is already booked. Please choose a different time.",
256980
+ conflictingPeriods: busyPeriods
256981
+ }), true;
256982
+ }
256983
+ } else {
256984
+ const fbErr = await fbRes.text().catch(() => "");
256985
+ console.warn(
256986
+ `FreeBusy pre-check failed (${fbRes.status}), proceeding without check: ${fbErr}`
256987
+ );
256988
+ }
256989
+ } catch (fbError) {
256990
+ console.warn(
256991
+ "FreeBusy pre-check threw an error, proceeding without check:",
256992
+ fbError instanceof Error ? fbError.message : String(fbError)
256993
+ );
256994
+ }
256995
+ }
256996
+ const eventBody = {
256997
+ summary: title,
256998
+ start: isAllDay ? { date: start } : { dateTime: start },
256999
+ end: isAllDay ? { date: end } : { dateTime: end }
257000
+ };
257001
+ if (description) eventBody.description = description;
257002
+ if (resourceEmail) {
257003
+ eventBody.attendees = [{ email: resourceEmail, resource: true }];
257004
+ }
257005
+ if (rrule) {
257006
+ eventBody.recurrence = [rrule];
257007
+ }
257008
+ const createRes = await adminFetch(
257009
+ token,
257010
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events`,
257011
+ {
257012
+ method: "POST",
257013
+ headers: { "Content-Type": "application/json" },
257014
+ body: JSON.stringify(eventBody)
257015
+ }
257016
+ );
257017
+ if (!createRes.ok) {
257018
+ const err = await createRes.text().catch(() => "");
257019
+ return respondJson2(res, 200, { ok: false, error: `Calendar create failed: ${createRes.status} ${err}` }), true;
257020
+ }
257021
+ const calEvent = await createRes.json();
257022
+ const calendarEventId = String(calEvent.id || "");
257023
+ let resourceName = "";
257024
+ if (resourceEmail) {
257025
+ try {
257026
+ const resources = await fetchResources(token);
257027
+ const match = resources.find((r2) => r2.resourceEmail === resourceEmail);
257028
+ if (match) resourceName = match.resourceName;
257029
+ } catch {
257030
+ }
257031
+ }
257032
+ const reservationId = generateReservationId();
257033
+ const nowIso40 = (/* @__PURE__ */ new Date()).toISOString();
257034
+ const reservation = {
257035
+ reservationId,
257036
+ calendarEventId,
257037
+ resourceCalendarId: resourceEmail,
257038
+ resourceName,
257039
+ startTime: start,
257040
+ endTime: end,
257041
+ title,
257042
+ isAllDay,
257043
+ status: "confirmed",
257044
+ bookerName,
257045
+ bookerPhone,
257046
+ bookerEmail,
257047
+ callSid,
257048
+ bookedVia,
257049
+ createdAt: nowIso40,
257050
+ updatedAt: nowIso40
257051
+ };
257052
+ if (rrule && recurrenceType) {
257053
+ reservation.isRecurring = true;
257054
+ reservation.recurrenceRule = rrule;
257055
+ reservation.recurrenceType = recurrenceType;
257056
+ reservation.recurrenceCount = recurrenceCount || null;
257057
+ reservation.recurrenceUntil = recurrenceUntil || null;
257058
+ }
257059
+ try {
257060
+ if (resourceEmail) {
257061
+ await db.runTransaction(async (transaction) => {
257062
+ const conflictQuery = db.collection("reservations").where("resourceCalendarId", "==", resourceEmail).where("status", "==", "confirmed").where("startTime", "<", end);
257063
+ const conflictSnap = await transaction.get(conflictQuery);
257064
+ const hasConflict = conflictSnap.docs.some((doc) => {
257065
+ const data = doc.data();
257066
+ return data.endTime > start;
257067
+ });
257068
+ if (hasConflict) {
257069
+ throw new Error("RESERVATION_CONFLICT");
257070
+ }
257071
+ transaction.set(
257072
+ db.collection("reservations").doc(reservationId),
257073
+ reservation
257074
+ );
257075
+ });
257076
+ } else {
257077
+ await db.collection("reservations").doc(reservationId).set(reservation);
257078
+ }
257079
+ } catch (err) {
257080
+ const msg = err instanceof Error ? err.message : String(err);
257081
+ if (calendarEventId) {
257082
+ try {
257083
+ await adminFetch(
257084
+ token,
257085
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
257086
+ { method: "DELETE" }
257087
+ );
257088
+ } catch (rollbackErr) {
257089
+ console.warn(
257090
+ "Failed to rollback Calendar event after Firestore error:",
257091
+ rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)
257092
+ );
257093
+ }
257094
+ }
257095
+ if (msg === "RESERVATION_CONFLICT") {
257096
+ return respondJson2(res, 409, {
257097
+ ok: false,
257098
+ error: "slot_conflict",
257099
+ message: "The requested time slot is already booked. Please choose a different time."
257100
+ }), true;
257101
+ }
257102
+ return respondJson2(res, 200, {
257103
+ ok: false,
257104
+ error: `Firestore write failed: ${msg}`
257105
+ }), true;
257106
+ }
257107
+ return respondJson2(res, 200, {
257108
+ ok: true,
257109
+ reservationId,
257110
+ calendarEventId: calEvent.id,
257111
+ resourceName,
257112
+ start,
257113
+ end,
257114
+ title,
257115
+ status: "confirmed",
257116
+ ...rrule ? {
257117
+ isRecurring: true,
257118
+ recurrenceType,
257119
+ recurrenceRule: rrule
257120
+ } : {}
257121
+ }), true;
257122
+ }
257123
+ if (method === "GET" && /^\/api\/calendar\/reserve\/[^/]+\/instances$/.test(pathname)) {
257124
+ const parts = pathname.split("/");
257125
+ const reservationId = decodeURIComponent(parts[4]);
257126
+ const db = await getFirestore2();
257127
+ if (!db) return firestoreUnavailable(res);
257128
+ const docSnap = await db.collection("reservations").doc(reservationId).get();
257129
+ if (!docSnap.exists) {
257130
+ return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
257131
+ }
257132
+ const data = docSnap.data();
257133
+ if (!data.isRecurring || !data.calendarEventId) {
257134
+ return respondJson2(res, 400, { ok: false, error: "Not a recurring reservation" }), true;
257135
+ }
257136
+ const token = await getToken2();
257137
+ if (!token) return notConnected2(res);
257138
+ const calendarId = String(data.resourceCalendarId || "") || "primary";
257139
+ const eventId = String(data.calendarEventId);
257140
+ const instancesRes = await adminFetch(
257141
+ token,
257142
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}/instances?maxResults=50`
257143
+ );
257144
+ if (!instancesRes.ok) {
257145
+ const errText = await instancesRes.text().catch(() => "");
257146
+ return respondJson2(res, 502, {
257147
+ ok: false,
257148
+ error: `Failed to fetch instances: ${instancesRes.status}`,
257149
+ detail: errText
257150
+ }), true;
257151
+ }
257152
+ const instancesData = await instancesRes.json();
257153
+ const items = Array.isArray(instancesData.items) ? instancesData.items : [];
257154
+ const instances = items.map((item) => {
257155
+ const startObj = item.start;
257156
+ const endObj = item.end;
257157
+ return {
257158
+ eventId: String(item.id || ""),
257159
+ start: startObj?.dateTime || startObj?.date || "",
257160
+ end: endObj?.dateTime || endObj?.date || "",
257161
+ status: String(item.status || "confirmed")
257162
+ };
257163
+ });
257164
+ return respondJson2(res, 200, { ok: true, reservationId, instances }), true;
257165
+ }
257166
+ if (method === "PATCH" && pathname.startsWith("/api/calendar/reserve/")) {
257167
+ const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
257168
+ if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
257169
+ const reqBody = await parseJsonBody2(req);
257170
+ if (!reqBody) return respondJson2(res, 400, { ok: false, error: "Request body is required" }), true;
257171
+ const start = String(reqBody.start || "").trim();
257172
+ const end = String(reqBody.end || "").trim();
257173
+ const resourceEmail = reqBody.resourceEmail ? String(reqBody.resourceEmail).trim() : "";
257174
+ const isAllDay = reqBody.isAllDay !== void 0 ? reqBody.isAllDay === true : void 0;
257175
+ if (!start || !end) {
257176
+ return respondJson2(res, 400, { ok: false, error: "start and end are required" }), true;
257177
+ }
257178
+ const db = await getFirestore2();
257179
+ if (!db) return firestoreUnavailable(res);
257180
+ const docRef = db.collection("reservations").doc(reservationId);
257181
+ let docSnap = await docRef.get();
257182
+ if (!docSnap.exists && reservationId.startsWith("gcal-")) {
257183
+ const gcalEventId = reservationId.slice(5);
257184
+ const calendarIdForUpsert = resourceEmail || String(reqBody.calendarId || "").trim();
257185
+ if (!calendarIdForUpsert) {
257186
+ return respondJson2(res, 400, { ok: false, error: "calendarId is required for direct calendar bookings" }), true;
257187
+ }
257188
+ const upsertToken = await getToken2();
257189
+ if (!upsertToken) return notConnected2(res);
257190
+ const eventRes = await adminFetch(
257191
+ upsertToken,
257192
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarIdForUpsert)}/events/${encodeURIComponent(gcalEventId)}`
257193
+ );
257194
+ if (!eventRes.ok) {
257195
+ return respondJson2(res, 404, { ok: false, error: "Calendar event not found" }), true;
257196
+ }
257197
+ const gcalEvent = await eventRes.json();
257198
+ const gcalStart = gcalEvent.start;
257199
+ const gcalEnd = gcalEvent.end;
257200
+ const creator = gcalEvent.creator;
257201
+ const isAllDay2 = Boolean(gcalStart?.date && !gcalStart?.dateTime);
257202
+ const upsertDoc = {
257203
+ reservationId,
257204
+ calendarEventId: gcalEventId,
257205
+ resourceCalendarId: calendarIdForUpsert,
257206
+ resourceName: String(gcalEvent.location || ""),
257207
+ startTime: gcalStart?.dateTime || gcalStart?.date || "",
257208
+ endTime: gcalEnd?.dateTime || gcalEnd?.date || "",
257209
+ title: String(gcalEvent.summary || "Direct Calendar Booking"),
257210
+ isAllDay: isAllDay2,
257211
+ status: "confirmed",
257212
+ bookerName: creator?.displayName || creator?.email || "",
257213
+ bookerPhone: "",
257214
+ bookerEmail: creator?.email || "",
257215
+ bookedVia: "direct_calendar",
257216
+ callSid: "",
257217
+ createdAt: String(gcalEvent.created || (/* @__PURE__ */ new Date()).toISOString()),
257218
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
257219
+ };
257220
+ await docRef.set(upsertDoc);
257221
+ docSnap = await docRef.get();
257222
+ }
257223
+ if (!docSnap.exists) {
257224
+ return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
257225
+ }
257226
+ const data = docSnap.data();
257227
+ if (data.status !== "confirmed") {
257228
+ return respondJson2(res, 400, { ok: false, error: "Only confirmed reservations can be modified" }), true;
257229
+ }
257230
+ const token = await getToken2();
257231
+ if (!token) return notConnected2(res);
257232
+ const effectiveResource = resourceEmail || String(data.resourceCalendarId || "");
257233
+ const calendarId = effectiveResource || "primary";
257234
+ const calendarEventId = String(data.calendarEventId || "");
257235
+ if (effectiveResource) {
257236
+ try {
257237
+ const fbBody = {
257238
+ timeMin: start,
257239
+ timeMax: end,
257240
+ timeZone: DEFAULT_TIMEZONE,
257241
+ items: [{ id: calendarId }]
257242
+ };
257243
+ const fbRes = await adminFetch(token, `${CALENDAR_API_BASE}/freeBusy`, {
257244
+ method: "POST",
257245
+ headers: { "Content-Type": "application/json" },
257246
+ body: JSON.stringify(fbBody)
257247
+ });
257248
+ if (fbRes.ok) {
257249
+ const fbData = await fbRes.json();
257250
+ const fbCalendars = fbData.calendars || {};
257251
+ const busyPeriods = fbCalendars[calendarId]?.busy || [];
257252
+ const currentStart = String(data.startTime || "");
257253
+ const currentEnd = String(data.endTime || "");
257254
+ const externalConflicts = busyPeriods.filter((b) => {
257255
+ const bStart = new Date(b.start).getTime();
257256
+ const bEnd = new Date(b.end).getTime();
257257
+ const curStart = currentStart ? new Date(currentStart).getTime() : 0;
257258
+ const curEnd = currentEnd ? new Date(currentEnd).getTime() : 0;
257259
+ return !(bStart === curStart && bEnd === curEnd);
257260
+ });
257261
+ if (externalConflicts.length > 0) {
257262
+ return respondJson2(res, 409, {
257263
+ ok: false,
257264
+ error: "slot_conflict",
257265
+ message: "The requested time slot is already booked by another reservation. Please choose a different time.",
257266
+ conflictingPeriods: externalConflicts
257267
+ }), true;
257268
+ }
257269
+ } else {
257270
+ const fbErr = await fbRes.text().catch(() => "");
257271
+ console.warn(
257272
+ `FreeBusy pre-check for modify failed (${fbRes.status}), proceeding: ${fbErr}`
257273
+ );
257274
+ }
257275
+ } catch (fbError) {
257276
+ console.warn(
257277
+ "FreeBusy pre-check for modify threw an error, proceeding:",
257278
+ fbError instanceof Error ? fbError.message : String(fbError)
257279
+ );
257280
+ }
257281
+ }
257282
+ if (calendarEventId) {
257283
+ const effectiveIsAllDay = isAllDay !== void 0 ? isAllDay : Boolean(data.isAllDay);
257284
+ const patchBody = {};
257285
+ if (start) patchBody.start = effectiveIsAllDay ? { date: start, dateTime: null } : { dateTime: start, date: null };
257286
+ if (end) patchBody.end = effectiveIsAllDay ? { date: end, dateTime: null } : { dateTime: end, date: null };
257287
+ if (resourceEmail && resourceEmail !== String(data.resourceCalendarId || "")) {
257288
+ patchBody.attendees = [{ email: resourceEmail, resource: true }];
257289
+ }
257290
+ const patchRes = await adminFetch(
257291
+ token,
257292
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
257293
+ { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patchBody) }
257294
+ );
257295
+ if (!patchRes.ok) {
257296
+ const errText = await patchRes.text().catch(() => "");
257297
+ return respondJson2(res, 502, { ok: false, error: `Calendar update failed: ${patchRes.status}`, detail: errText }), true;
257298
+ }
257299
+ }
257300
+ const updateFields = {
257301
+ startTime: start,
257302
+ endTime: end,
257303
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
257304
+ };
257305
+ if (isAllDay !== void 0) {
257306
+ updateFields.isAllDay = isAllDay;
257307
+ }
257308
+ if (resourceEmail && resourceEmail !== String(data.resourceCalendarId || "")) {
257309
+ updateFields.resourceCalendarId = resourceEmail;
257310
+ try {
257311
+ const resources = await fetchResources(token);
257312
+ const matched = resources.find((r2) => r2.resourceEmail === resourceEmail);
257313
+ if (matched) updateFields.resourceName = matched.resourceName || resourceEmail;
257314
+ } catch {
257315
+ }
257316
+ }
257317
+ await docRef.update(updateFields);
257318
+ return respondJson2(res, 200, {
257319
+ ok: true,
257320
+ reservationId,
257321
+ start,
257322
+ end,
257323
+ resourceName: String(updateFields.resourceName || data.resourceName || ""),
257324
+ status: "confirmed"
257325
+ }), true;
257326
+ }
257327
+ if (method === "DELETE" && pathname.startsWith("/api/calendar/reserve/")) {
257328
+ const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
257329
+ if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
257330
+ const db = await getFirestore2();
257331
+ if (!db) return firestoreUnavailable(res);
257332
+ const url = new URL(req.url || "/", "http://localhost");
257333
+ const docRef = db.collection("reservations").doc(reservationId);
257334
+ let docSnap = await docRef.get();
257335
+ if (!docSnap.exists && reservationId.startsWith("gcal-")) {
257336
+ const gcalEventId = reservationId.slice(5);
257337
+ const gcalCalendarId = url.searchParams.get("calendarId") || "";
257338
+ if (!gcalCalendarId) {
257339
+ return respondJson2(res, 400, { ok: false, error: "calendarId query param is required for direct calendar bookings" }), true;
257340
+ }
257341
+ const upsertToken = await getToken2();
257342
+ if (!upsertToken) return notConnected2(res);
257343
+ const eventRes = await adminFetch(
257344
+ upsertToken,
257345
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(gcalCalendarId)}/events/${encodeURIComponent(gcalEventId)}`
257346
+ );
257347
+ if (!eventRes.ok) {
257348
+ return respondJson2(res, 404, { ok: false, error: "Calendar event not found" }), true;
257349
+ }
257350
+ const gcalEvent = await eventRes.json();
257351
+ const gcalStart = gcalEvent.start;
257352
+ const gcalEnd = gcalEvent.end;
257353
+ const creator = gcalEvent.creator;
257354
+ const isAllDay = Boolean(gcalStart?.date && !gcalStart?.dateTime);
257355
+ const upsertDoc = {
257356
+ reservationId,
257357
+ calendarEventId: gcalEventId,
257358
+ resourceCalendarId: gcalCalendarId,
257359
+ resourceName: String(gcalEvent.location || ""),
257360
+ startTime: gcalStart?.dateTime || gcalStart?.date || "",
257361
+ endTime: gcalEnd?.dateTime || gcalEnd?.date || "",
257362
+ title: String(gcalEvent.summary || "Direct Calendar Booking"),
257363
+ isAllDay,
257364
+ status: "confirmed",
257365
+ bookerName: creator?.displayName || creator?.email || "",
257366
+ bookerPhone: "",
257367
+ bookerEmail: creator?.email || "",
257368
+ bookedVia: "direct_calendar",
257369
+ callSid: "",
257370
+ createdAt: String(gcalEvent.created || (/* @__PURE__ */ new Date()).toISOString()),
257371
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
257372
+ };
257373
+ await docRef.set(upsertDoc);
257374
+ docSnap = await docRef.get();
257375
+ }
257376
+ if (!docSnap.exists) {
257377
+ return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
257378
+ }
257379
+ const data = docSnap.data();
257380
+ const scope = url.searchParams.get("scope") || "all";
257381
+ const instanceEventId = url.searchParams.get("instanceEventId") || "";
257382
+ const calendarEventId = String(data.calendarEventId || "");
257383
+ const calendarId = String(data.resourceCalendarId || "") || "primary";
257384
+ const isRecurring = Boolean(data.isRecurring);
257385
+ const token = await getToken2();
257386
+ if (isRecurring && scope === "single") {
257387
+ if (!instanceEventId) {
257388
+ return respondJson2(res, 400, {
257389
+ ok: false,
257390
+ error: "instanceEventId is required when scope=single"
257391
+ }), true;
257392
+ }
257393
+ if (token) {
257394
+ try {
257395
+ await adminFetch(
257396
+ token,
257397
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(instanceEventId)}`,
257398
+ { method: "DELETE" }
257399
+ );
257400
+ } catch {
257401
+ }
257402
+ }
257403
+ return respondJson2(res, 200, {
257404
+ ok: true,
257405
+ reservationId,
257406
+ cancelledInstanceEventId: instanceEventId,
257407
+ scope: "single",
257408
+ status: "instance_cancelled"
257409
+ }), true;
257410
+ } else if (isRecurring && scope === "this_and_following") {
257411
+ if (!instanceEventId) {
257412
+ return respondJson2(res, 400, {
257413
+ ok: false,
257414
+ error: "instanceEventId is required when scope=this_and_following to determine the cut-off date"
257415
+ }), true;
257416
+ }
257417
+ if (token && calendarEventId) {
257418
+ try {
257419
+ const instanceRes = await adminFetch(
257420
+ token,
257421
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(instanceEventId)}`
257422
+ );
257423
+ if (instanceRes.ok) {
257424
+ const instanceData = await instanceRes.json();
257425
+ const instanceStart = instanceData.start?.dateTime || "";
257426
+ if (instanceStart) {
257427
+ const cutoffDate = new Date(instanceStart);
257428
+ cutoffDate.setDate(cutoffDate.getDate() - 1);
257429
+ const untilStr = cutoffDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
257430
+ const parentRes = await adminFetch(
257431
+ token,
257432
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`
257433
+ );
257434
+ if (parentRes.ok) {
257435
+ const parentData = await parentRes.json();
257436
+ const existingRecurrence = Array.isArray(parentData.recurrence) ? parentData.recurrence : [];
257437
+ const updatedRecurrence = existingRecurrence.map((rule) => {
257438
+ if (!rule.startsWith("RRULE:")) return rule;
257439
+ const parts = rule.split(";").filter(
257440
+ (p) => !p.startsWith("COUNT=") && !p.startsWith("UNTIL=")
257441
+ );
257442
+ return [...parts, `UNTIL=${untilStr}`].join(";");
257443
+ });
257444
+ await adminFetch(
257445
+ token,
257446
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
257447
+ {
257448
+ method: "PATCH",
257449
+ headers: { "Content-Type": "application/json" },
257450
+ body: JSON.stringify({ recurrence: updatedRecurrence })
257451
+ }
257452
+ );
257453
+ }
257454
+ }
257455
+ }
257456
+ } catch (err) {
257457
+ console.warn(
257458
+ "Failed to update RRULE for this_and_following cancel:",
257459
+ err instanceof Error ? err.message : String(err)
257460
+ );
257461
+ }
257462
+ }
257463
+ await docRef.update({
257464
+ recurrenceUntil: instanceEventId,
257465
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
257466
+ });
257467
+ return respondJson2(res, 200, {
257468
+ ok: true,
257469
+ reservationId,
257470
+ scope: "this_and_following",
257471
+ status: "truncated"
257472
+ }), true;
257473
+ } else {
257474
+ if (calendarEventId && token) {
257475
+ try {
257476
+ await adminFetch(
257477
+ token,
257478
+ `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(calendarEventId)}`,
257479
+ { method: "DELETE" }
257480
+ );
257481
+ } catch {
257482
+ }
257483
+ }
257484
+ await docRef.update({
257485
+ status: "cancelled",
257486
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
257487
+ });
257488
+ return respondJson2(res, 200, {
257489
+ ok: true,
257490
+ reservationId,
257491
+ scope: "all",
257492
+ status: "cancelled"
257493
+ }), true;
257494
+ }
257495
+ }
257496
+ if (method === "GET" && pathname.startsWith("/api/calendar/reserve/")) {
257497
+ const reservationId = decodeURIComponent(pathname.slice("/api/calendar/reserve/".length));
257498
+ if (!reservationId) return respondJson2(res, 400, { ok: false, error: "reservationId is required" }), true;
257499
+ const db = await getFirestore2();
257500
+ if (!db) return firestoreUnavailable(res);
257501
+ const docSnap = await db.collection("reservations").doc(reservationId).get();
257502
+ if (!docSnap.exists) {
257503
+ return respondJson2(res, 404, { ok: false, error: "Reservation not found" }), true;
257504
+ }
257505
+ return respondJson2(res, 200, { ok: true, reservation: docSnap.data() }), true;
257506
+ }
257507
+ if (method === "GET" && pathname === "/api/calendar/reservations") {
257508
+ const db = await getFirestore2();
257509
+ if (!db) return firestoreUnavailable(res);
257510
+ const url = new URL(req.url || "/", "http://localhost");
257511
+ const status = url.searchParams.get("status") || "";
257512
+ const date = url.searchParams.get("date") || "";
257513
+ const resourceEmail = url.searchParams.get("resourceEmail") || "";
257514
+ const buildingId = url.searchParams.get("buildingId") || "";
257515
+ const limit = Math.max(1, Math.min(200, Number(url.searchParams.get("limit") || DEFAULT_RESERVATION_LIMIT) || DEFAULT_RESERVATION_LIMIT));
257516
+ let query = db.collection("reservations");
257517
+ if (status) {
257518
+ query = query.where("status", "==", status);
257519
+ }
257520
+ if (resourceEmail) {
257521
+ query = query.where("resourceCalendarId", "==", resourceEmail);
257522
+ }
257523
+ if (date) {
257524
+ const dayStart = `${date}T00:00:00`;
257525
+ const dayEnd = `${date}T23:59:59`;
257526
+ query = query.where("startTime", ">=", dayStart).where("startTime", "<=", dayEnd + "Z");
257527
+ }
257528
+ query = query.orderBy("createdAt", "desc").limit(limit);
257529
+ let firestoreDocs;
257530
+ try {
257531
+ const snap = await query.get();
257532
+ firestoreDocs = snap.docs;
257533
+ } catch (err) {
257534
+ const msg = err instanceof Error ? err.message : String(err);
257535
+ return respondJson2(res, 200, { ok: false, error: `Firestore query failed: ${msg}` }), true;
257536
+ }
257537
+ const token = await getToken2();
257538
+ if (!token) {
257539
+ if (firestoreDocs.length === 0) {
257540
+ return respondJson2(res, 200, { ok: true, reservations: [], count: 0, dataSource: "firestore_fallback" }), true;
257541
+ }
257542
+ const reservations = firestoreDocs.map((doc) => doc.data());
257543
+ return respondJson2(res, 200, {
257544
+ ok: true,
257545
+ reservations,
257546
+ count: reservations.length,
257547
+ dataSource: "firestore_fallback"
257548
+ }), true;
257549
+ }
257550
+ const docsWithEventId = [];
257551
+ const docsWithoutEventId = [];
257552
+ for (const doc of firestoreDocs) {
257553
+ const data = doc.data();
257554
+ if (data.calendarEventId) {
257555
+ docsWithEventId.push(data);
257556
+ } else {
257557
+ docsWithoutEventId.push(data);
257558
+ }
257559
+ }
257560
+ const byCalendar = /* @__PURE__ */ new Map();
257561
+ for (const doc of docsWithEventId) {
257562
+ const calId = doc.resourceCalendarId || "primary";
257563
+ const group = byCalendar.get(calId);
257564
+ if (group) {
257565
+ group.push(doc);
257566
+ } else {
257567
+ byCalendar.set(calId, [doc]);
257568
+ }
257569
+ }
257570
+ const additionalCalendarIds = [];
257571
+ if (resourceEmail && !byCalendar.has(resourceEmail)) {
257572
+ additionalCalendarIds.push(resourceEmail);
257573
+ }
257574
+ if (!resourceEmail) {
257575
+ try {
257576
+ const resources = await fetchResources(token, buildingId || void 0);
257577
+ for (const r2 of resources) {
257578
+ if (r2.resourceEmail && !byCalendar.has(r2.resourceEmail)) {
257579
+ additionalCalendarIds.push(r2.resourceEmail);
257580
+ }
257581
+ }
257582
+ } catch (err) {
257583
+ console.warn(
257584
+ "Failed to fetch resource calendars from Admin SDK:",
257585
+ err instanceof Error ? err.message : String(err)
257586
+ );
257587
+ }
257588
+ }
257589
+ for (const calId of additionalCalendarIds) {
257590
+ if (!byCalendar.has(calId)) {
257591
+ byCalendar.set(calId, []);
257592
+ }
257593
+ }
257594
+ let timeMin;
257595
+ let timeMax;
257596
+ if (date) {
257597
+ timeMin = `${date}T00:00:00+09:00`;
257598
+ timeMax = `${date}T23:59:59+09:00`;
257599
+ } else {
257600
+ const startTimes = docsWithEventId.map((d) => d.startTime).filter(Boolean).sort();
257601
+ if (startTimes.length > 0) {
257602
+ const earliest = new Date(startTimes[0]);
257603
+ earliest.setDate(earliest.getDate() - 1);
257604
+ const latest = new Date(startTimes[startTimes.length - 1]);
257605
+ latest.setDate(latest.getDate() + 1);
257606
+ timeMin = earliest.toISOString();
257607
+ timeMax = latest.toISOString();
257608
+ } else {
257609
+ const now = /* @__PURE__ */ new Date();
257610
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
257611
+ const thirtyDaysAhead = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1e3);
257612
+ timeMin = thirtyDaysAgo.toISOString();
257613
+ timeMax = thirtyDaysAhead.toISOString();
257614
+ }
257615
+ }
257616
+ const gcalEventMap = /* @__PURE__ */ new Map();
257617
+ let gcalFetchFailed = false;
257618
+ try {
257619
+ const calendarFetches = Array.from(byCalendar.keys()).map(async (calendarId) => {
257620
+ const eventsUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?timeMin=${encodeURIComponent(timeMin)}&timeMax=${encodeURIComponent(timeMax)}&singleEvents=true&orderBy=startTime&maxResults=250`;
257621
+ const eventsRes = await adminFetch(token, eventsUrl);
257622
+ if (!eventsRes.ok) {
257623
+ console.warn(
257624
+ `GCal events.list failed for calendar ${calendarId}: ${eventsRes.status}`
257625
+ );
257626
+ return;
257627
+ }
257628
+ const eventsData = await eventsRes.json();
257629
+ const items = Array.isArray(eventsData.items) ? eventsData.items : [];
257630
+ for (const item of items) {
257631
+ const eventId = String(item.id || "");
257632
+ if (!eventId) continue;
257633
+ const startObj = item.start;
257634
+ const endObj = item.end;
257635
+ const isAllDay = Boolean(startObj?.date && !startObj?.dateTime);
257636
+ const eventStart = startObj?.dateTime || startObj?.date || "";
257637
+ const eventEnd = endObj?.dateTime || endObj?.date || "";
257638
+ const creatorObj = item.creator;
257639
+ const organizerObj = item.organizer;
257640
+ gcalEventMap.set(eventId, {
257641
+ startTime: eventStart,
257642
+ endTime: eventEnd,
257643
+ title: String(item.summary || ""),
257644
+ isAllDay,
257645
+ gcalStatus: String(item.status || "confirmed"),
257646
+ creatorEmail: creatorObj?.email || "",
257647
+ creatorDisplayName: creatorObj?.displayName || "",
257648
+ organizerEmail: organizerObj?.email || "",
257649
+ organizerDisplayName: organizerObj?.displayName || "",
257650
+ location: String(item.location || ""),
257651
+ created: String(item.created || ""),
257652
+ updated: String(item.updated || ""),
257653
+ calendarId
257654
+ });
257655
+ }
257656
+ });
257657
+ await Promise.all(calendarFetches);
257658
+ } catch (err) {
257659
+ console.warn(
257660
+ "GCal fetch failed, falling back to Firestore:",
257661
+ err instanceof Error ? err.message : String(err)
257662
+ );
257663
+ gcalFetchFailed = true;
257664
+ }
257665
+ if (gcalFetchFailed) {
257666
+ const reservations = firestoreDocs.map((doc) => doc.data());
257667
+ return respondJson2(res, 200, {
257668
+ ok: true,
257669
+ reservations,
257670
+ count: reservations.length,
257671
+ dataSource: "firestore_fallback"
257672
+ }), true;
257673
+ }
257674
+ const mergedReservations = [];
257675
+ for (const fsDoc of docsWithEventId) {
257676
+ const gcalEvent = gcalEventMap.get(fsDoc.calendarEventId);
257677
+ if (gcalEvent) {
257678
+ const effectiveStatus = gcalEvent.gcalStatus === "cancelled" ? "cancelled" : fsDoc.status;
257679
+ mergedReservations.push({
257680
+ reservationId: fsDoc.reservationId,
257681
+ calendarEventId: fsDoc.calendarEventId,
257682
+ resourceCalendarId: fsDoc.resourceCalendarId,
257683
+ resourceName: fsDoc.resourceName,
257684
+ startTime: gcalEvent.startTime,
257685
+ // from GCal
257686
+ endTime: gcalEvent.endTime,
257687
+ // from GCal
257688
+ title: gcalEvent.title,
257689
+ // from GCal
257690
+ isAllDay: gcalEvent.isAllDay,
257691
+ status: effectiveStatus,
257692
+ bookerName: fsDoc.bookerName,
257693
+ // from Firestore
257694
+ bookerPhone: fsDoc.bookerPhone,
257695
+ // from Firestore
257696
+ bookerEmail: fsDoc.bookerEmail,
257697
+ // from Firestore
257698
+ callSid: fsDoc.callSid,
257699
+ // from Firestore
257700
+ bookedVia: fsDoc.bookedVia,
257701
+ // from Firestore
257702
+ createdAt: fsDoc.createdAt,
257703
+ ...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
257704
+ dataSource: "google_calendar"
257705
+ });
257706
+ } else {
257707
+ mergedReservations.push({
257708
+ reservationId: fsDoc.reservationId,
257709
+ calendarEventId: fsDoc.calendarEventId,
257710
+ resourceCalendarId: fsDoc.resourceCalendarId,
257711
+ resourceName: fsDoc.resourceName,
257712
+ startTime: fsDoc.startTime,
257713
+ // from Firestore (stale)
257714
+ endTime: fsDoc.endTime,
257715
+ // from Firestore (stale)
257716
+ title: fsDoc.title,
257717
+ // from Firestore (stale)
257718
+ isAllDay: false,
257719
+ status: fsDoc.status === "confirmed" ? "cancelled" : fsDoc.status,
257720
+ bookerName: fsDoc.bookerName,
257721
+ bookerPhone: fsDoc.bookerPhone,
257722
+ bookerEmail: fsDoc.bookerEmail,
257723
+ callSid: fsDoc.callSid,
257724
+ bookedVia: fsDoc.bookedVia,
257725
+ createdAt: fsDoc.createdAt,
257726
+ ...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
257727
+ dataSource: "stale"
257728
+ });
257729
+ }
257730
+ }
257731
+ for (const fsDoc of docsWithoutEventId) {
257732
+ mergedReservations.push({
257733
+ reservationId: fsDoc.reservationId,
257734
+ calendarEventId: "",
257735
+ resourceCalendarId: fsDoc.resourceCalendarId,
257736
+ resourceName: fsDoc.resourceName,
257737
+ startTime: fsDoc.startTime,
257738
+ endTime: fsDoc.endTime,
257739
+ title: fsDoc.title,
257740
+ isAllDay: false,
257741
+ status: fsDoc.status,
257742
+ bookerName: fsDoc.bookerName,
257743
+ bookerPhone: fsDoc.bookerPhone,
257744
+ bookerEmail: fsDoc.bookerEmail,
257745
+ callSid: fsDoc.callSid,
257746
+ bookedVia: fsDoc.bookedVia,
257747
+ createdAt: fsDoc.createdAt,
257748
+ ...fsDoc.updatedAt ? { updatedAt: fsDoc.updatedAt } : {},
257749
+ dataSource: "firestore_fallback"
257750
+ });
257751
+ }
257752
+ const matchedEventIds = new Set(docsWithEventId.map((d) => d.calendarEventId));
257753
+ for (const [eventId, gcalEvent] of gcalEventMap) {
257754
+ if (matchedEventIds.has(eventId)) continue;
257755
+ if (status) {
257756
+ const gcalEffectiveStatus = gcalEvent.gcalStatus === "cancelled" ? "cancelled" : "confirmed";
257757
+ if (gcalEffectiveStatus !== status) continue;
257758
+ }
257759
+ mergedReservations.push({
257760
+ reservationId: `gcal-${eventId}`,
257761
+ calendarEventId: eventId,
257762
+ resourceCalendarId: gcalEvent.calendarId,
257763
+ resourceName: gcalEvent.location || gcalEvent.organizerDisplayName || "",
257764
+ startTime: gcalEvent.startTime,
257765
+ endTime: gcalEvent.endTime,
257766
+ title: gcalEvent.title || "Direct Calendar Booking",
257767
+ isAllDay: gcalEvent.isAllDay,
257768
+ status: gcalEvent.gcalStatus === "cancelled" ? "cancelled" : "confirmed",
257769
+ bookerName: gcalEvent.creatorDisplayName || gcalEvent.creatorEmail || "",
257770
+ bookerPhone: "",
257771
+ bookerEmail: gcalEvent.creatorEmail,
257772
+ callSid: "",
257773
+ bookedVia: "direct_calendar",
257774
+ createdAt: gcalEvent.created,
257775
+ updatedAt: gcalEvent.updated,
257776
+ dataSource: "google_calendar_only"
257777
+ });
257778
+ }
257779
+ return respondJson2(res, 200, {
257780
+ ok: true,
257781
+ reservations: mergedReservations,
257782
+ count: mergedReservations.length,
257783
+ dataSource: "google_calendar"
257784
+ }), true;
257785
+ }
257786
+ return false;
257787
+ }
257788
+ var CALENDAR_API_BASE, DEFAULT_WORK_START_HOUR, DEFAULT_WORK_END_HOUR, DEFAULT_DURATION_MINUTES, DEFAULT_RESERVATION_LIMIT, SLOT_STEP_MS, DEFAULT_BUFFER_MINUTES, TOP_SLOT_COUNT, DEFAULT_TIMEZONE, _firestoreInstance2, _firestoreInitPromise2, VALID_RECURRENCE_TYPES;
257789
+ var init_calendar_api_routes = __esm({
257790
+ "services/desktop/routes/calendar-api-routes.ts"() {
257791
+ init_desktop_server_helpers();
257792
+ init_google_oauth();
257793
+ init_date_tz();
257794
+ init_admin_resources();
257795
+ CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3";
257796
+ DEFAULT_WORK_START_HOUR = 9;
257797
+ DEFAULT_WORK_END_HOUR = 18;
257798
+ DEFAULT_DURATION_MINUTES = 60;
257799
+ DEFAULT_RESERVATION_LIMIT = 50;
257800
+ SLOT_STEP_MS = 5 * 60 * 1e3;
257801
+ DEFAULT_BUFFER_MINUTES = 10;
257802
+ TOP_SLOT_COUNT = 3;
257803
+ DEFAULT_TIMEZONE = "Asia/Tokyo";
257804
+ _firestoreInstance2 = null;
257805
+ _firestoreInitPromise2 = null;
257806
+ VALID_RECURRENCE_TYPES = /* @__PURE__ */ new Set(["daily", "weekly", "biweekly", "monthly"]);
257807
+ }
257808
+ });
256285
257809
  function resolveMaxConcurrency() {
256286
257810
  const envRaw = String(process.env.MARIA_DESKTOP_MAX_CONCURRENCY || "").trim();
256287
257811
  if (envRaw) {
@@ -257435,6 +258959,10 @@ function createDesktopRequestHandler(s2) {
257435
258959
  const handled = await handlePhoneRoute(method, pathname, req, res);
257436
258960
  if (handled) return;
257437
258961
  }
258962
+ if (pathname.startsWith("/api/calendar/")) {
258963
+ const handled = await handleCalendarApiRoute(method, pathname, req, res);
258964
+ if (handled) return;
258965
+ }
257438
258966
  {
257439
258967
  const handled = await handleGuiCompatRoute(method, pathname, url, req, res, cwd, s2);
257440
258968
  if (handled) return;
@@ -257649,6 +259177,7 @@ var init_desktop_server = __esm({
257649
259177
  init_google_oauth_routes();
257650
259178
  init_google_api_routes();
257651
259179
  init_phone_routes();
259180
+ init_calendar_api_routes();
257652
259181
  init_desktop_server_helpers();
257653
259182
  init_desktop_data_readers();
257654
259183
  init_desktop_job_queue();
@@ -257991,7 +259520,7 @@ function createSpreadsheetField() {
257991
259520
  const wantsJson = hasLiteFlag(ctx.parsed, "json");
257992
259521
  const titleOpt = typeof ctx.parsed.options.title === "string" ? String(ctx.parsed.options.title).trim() : "";
257993
259522
  const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
257994
- const outBaseRel = (outBase || "spreadsheet").replace(/\\/g, "/");
259523
+ const outBaseRel = (outBase || ".maria/desktop/spreadsheet").replace(/\\/g, "/");
257995
259524
  const sheetsN = parseSheetsCount(ctx.parsed.options.sheets, 1);
257996
259525
  const stamp = nowStamp3();
257997
259526
  const outDirRel = path87__namespace.posix.join(
@@ -259658,7 +261187,7 @@ function createPaperField() {
259658
261187
  const n = typeof raw === "string" ? Number(raw) : typeof raw === "number" ? raw : 6;
259659
261188
  return Number.isFinite(n) ? Math.max(3, Math.min(20, Math.floor(n))) : 6;
259660
261189
  })();
259661
- const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "paper").replace(/\\/g, "/") || "paper";
261190
+ const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/paper").replace(/\\/g, "/") || ".maria/desktop/paper";
259662
261191
  const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
259663
261192
  const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
259664
261193
  const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
@@ -260163,7 +261692,7 @@ function createExamField() {
260163
261692
  const inlineArgs = ctx.parsed.args.join(" ").trim();
260164
261693
  const langRaw = typeof ctx.parsed.options.lang === "string" ? String(ctx.parsed.options.lang).trim().toLowerCase() : "";
260165
261694
  const langExplicit = langRaw === "ja" || langRaw === "jp" ? "ja" : langRaw === "en" ? "en" : "";
260166
- const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "exam").replace(/\\/g, "/") || "exam";
261695
+ const outBaseRel = (typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : ".maria/desktop/exam").replace(/\\/g, "/") || ".maria/desktop/exam";
260167
261696
  const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
260168
261697
  const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "pdf") && !hasLiteFlag(ctx.parsed, "docx");
260169
261698
  const planOnly = hasLiteFlag(ctx.parsed, "plan-only") || hasLiteFlag(ctx.parsed, "dry-run");
@@ -262207,25 +263736,32 @@ async function queryFreeBusy(token, emails, from, to, signal) {
262207
263736
  }
262208
263737
  async function findMeetingSlots(token, required, optional, durationMin, dateFrom, dateTo, workStart, workEnd, signal, extra) {
262209
263738
  let resourceCalendarIds = [];
263739
+ const resourceNameMap = {};
262210
263740
  if (extra?.resourceId) {
262211
263741
  resourceCalendarIds = [extra.resourceId];
262212
- } else if (extra?.buildingId || extra?.capacity) {
262213
263742
  try {
262214
- const adminBaseUrl = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources/calendars";
262215
- const params = new URLSearchParams({ maxResults: "200" });
262216
- if (extra?.buildingId) params.set("query", `buildingId="${extra.buildingId}"`);
262217
- const rRes = await fetch(`${adminBaseUrl}?${params}`, {
262218
- headers: { Authorization: `Bearer ${token}` },
262219
- ...signal ? { signal } : {}
262220
- });
263743
+ const qParams = new URLSearchParams({ maxResults: "5", query: `resourceEmail="${extra.resourceId}"` });
263744
+ const rRes = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/calendars?${qParams}`, signal ? { signal } : void 0);
262221
263745
  if (rRes.ok) {
262222
263746
  const rData = await rRes.json();
262223
263747
  const items = Array.isArray(rData.items) ? rData.items : [];
262224
- const filtered = items.filter((r2) => {
262225
- if (extra?.capacity && typeof r2.capacity === "number" && r2.capacity < extra.capacity) return false;
262226
- return true;
262227
- });
262228
- resourceCalendarIds = filtered.map((r2) => String(r2.resourceEmail || "")).filter(Boolean);
263748
+ for (const r2 of items) {
263749
+ const email = String(r2.resourceEmail || "");
263750
+ const name = String(r2.resourceName || r2.generatedResourceName || "");
263751
+ if (email && name) resourceNameMap[email] = name;
263752
+ }
263753
+ }
263754
+ } catch {
263755
+ }
263756
+ } else if (extra?.buildingId || extra?.capacity) {
263757
+ try {
263758
+ const result = await listResourcesRaw(token, { buildingId: extra?.buildingId, capacity: extra?.capacity }, signal);
263759
+ if (result.ok && result.data) {
263760
+ resourceCalendarIds = result.data.map((r2) => r2.resourceEmail).filter(Boolean);
263761
+ for (const r2 of result.data) {
263762
+ const name = r2.resourceName || r2.generatedResourceName || "";
263763
+ if (r2.resourceEmail && name) resourceNameMap[r2.resourceEmail] = name;
263764
+ }
262229
263765
  }
262230
263766
  } catch {
262231
263767
  }
@@ -262277,6 +263813,7 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
262277
263813
  let requiredOk = true;
262278
263814
  let availCount = 0;
262279
263815
  const missingOpt = [];
263816
+ const slotAvailResources = [];
262280
263817
  for (const email of allEmails) {
262281
263818
  const busy = busyMap[email] || [];
262282
263819
  const isBusy = busy.some((b) => b.s < slotE && b.e > slotS);
@@ -262288,6 +263825,9 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
262288
263825
  }
262289
263826
  } else {
262290
263827
  availCount++;
263828
+ if (resourceEmails.includes(email)) {
263829
+ slotAvailResources.push(email);
263830
+ }
262291
263831
  }
262292
263832
  }
262293
263833
  if (!requiredOk) continue;
@@ -262306,7 +263846,7 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
262306
263846
  }
262307
263847
  if (!isFinite(bufBefore)) bufBefore = 12 * 60 * 60 * 1e3;
262308
263848
  if (!isFinite(bufAfter)) bufAfter = 12 * 60 * 60 * 1e3;
262309
- candidates.push({ start: slotS, end: slotE, allAvailable: availCount === allEmails.length, availableCount: availCount, missingOptional: missingOpt, bufferBefore: bufBefore, bufferAfter: bufAfter });
263849
+ candidates.push({ start: slotS, end: slotE, allAvailable: availCount === allEmails.length, availableCount: availCount, missingOptional: missingOpt, bufferBefore: bufBefore, bufferAfter: bufAfter, availableResources: slotAvailResources });
262310
263850
  }
262311
263851
  }
262312
263852
  const hasSuf = (c) => c.bufferBefore >= minBufferMs && c.bufferAfter >= minBufferMs;
@@ -262347,6 +263887,10 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
262347
263887
  const label = c.allAvailable ? `All ${allEmails.length} available` : `${c.availableCount}/${allEmails.length} available`;
262348
263888
  lines.push(` #${i2 + 1}: ${s2.toLocaleString()} \u2192 ${e2.toLocaleTimeString()} (${label})`);
262349
263889
  if (c.missingOptional.length) lines.push(` Unavailable (optional): ${c.missingOptional.join(", ")}`);
263890
+ if (c.availableResources.length > 0) {
263891
+ const names = c.availableResources.map((e3) => resourceNameMap[e3] || e3);
263892
+ lines.push(` Available resources: ${names.join(", ")}`);
263893
+ }
262350
263894
  lines.push(` Buffer: ${Math.round(c.bufferBefore / 6e4)} min before, ${Math.round(c.bufferAfter / 6e4)} min after`);
262351
263895
  lines.push("");
262352
263896
  }
@@ -262364,7 +263908,9 @@ async function findMeetingSlots(token, required, optional, durationMin, dateFrom
262364
263908
  missingOptional: c.missingOptional,
262365
263909
  bufferBeforeMinutes: Math.round(c.bufferBefore / 6e4),
262366
263910
  bufferAfterMinutes: Math.round(c.bufferAfter / 6e4),
262367
- bufferMinutes: Math.round((c.bufferBefore + c.bufferAfter) / 6e4)
263911
+ bufferMinutes: Math.round((c.bufferBefore + c.bufferAfter) / 6e4),
263912
+ resourceEmails: c.availableResources,
263913
+ resourceNames: c.availableResources.map((e2) => resourceNameMap[e2] || e2)
262368
263914
  }))
262369
263915
  }
262370
263916
  };
@@ -262596,46 +264142,17 @@ function formatImportSummary(results, totalRecords, isDryRun) {
262596
264142
  }
262597
264143
  return lines;
262598
264144
  }
262599
- async function fetchExistingBuildingIds(token, signal) {
262600
- const ids = /* @__PURE__ */ new Set();
262601
- try {
262602
- const res = await fetch(`${ADMIN_RESOURCES_BASE}/buildings?maxResults=200`, {
262603
- headers: { Authorization: `Bearer ${token}` },
262604
- ...signal ? { signal } : {}
262605
- });
262606
- if (res.ok) {
262607
- const data = await res.json();
262608
- const buildings = Array.isArray(data.buildings) ? data.buildings : [];
262609
- for (const b of buildings) {
262610
- if (typeof b.buildingId === "string" && b.buildingId) {
262611
- ids.add(b.buildingId);
262612
- }
262613
- }
262614
- }
262615
- } catch {
262616
- }
262617
- return ids;
262618
- }
262619
- async function listResources(token, buildingId, signal) {
262620
- const params = new URLSearchParams({ maxResults: "200" });
262621
- if (buildingId) params.set("query", `buildingId="${buildingId}"`);
262622
- const res = await fetch(`${ADMIN_RESOURCES_BASE}/calendars?${params}`, {
262623
- headers: { Authorization: `Bearer ${token}` },
262624
- ...signal ? { signal } : {}
262625
- });
262626
- if (!res.ok) {
262627
- const err = await res.text().catch(() => "");
262628
- return { text: `Admin API error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
262629
- }
262630
- const data = await res.json();
262631
- const items = Array.isArray(data.items) ? data.items : [];
264145
+ async function fmtListResources(token, buildingId, signal) {
264146
+ const result = await listResourcesRaw(token, { buildingId }, signal);
264147
+ if (!result.ok) return { text: `Admin API error: ${result.error}`, json: { ok: false, error: result.error } };
264148
+ const items = result.data;
262632
264149
  if (items.length === 0) {
262633
264150
  const filter = buildingId ? ` (building: ${buildingId})` : "";
262634
264151
  return { text: `No resource calendars found${filter}.`, json: { ok: true, resources: [] } };
262635
264152
  }
262636
264153
  const lines = [`${items.length} resource calendar(s):`, ""];
262637
264154
  for (const r2 of items) {
262638
- const cap = typeof r2.capacity === "number" ? ` (capacity: ${r2.capacity})` : "";
264155
+ const cap = r2.capacity ? ` (capacity: ${r2.capacity})` : "";
262639
264156
  const bld = r2.buildingId ? ` [building: ${r2.buildingId}]` : "";
262640
264157
  lines.push(` ${r2.resourceName || r2.generatedResourceName || "Unnamed"}${cap}${bld}`);
262641
264158
  lines.push(` ID: ${r2.resourceId}`);
@@ -262646,22 +264163,16 @@ async function listResources(token, buildingId, signal) {
262646
264163
  }
262647
264164
  return { text: lines.join("\n"), json: { ok: true, resources: items } };
262648
264165
  }
262649
- async function getResource(token, resourceId, signal) {
262650
- const res = await fetch(`${ADMIN_RESOURCES_BASE}/calendars/${encodeURIComponent(resourceId)}`, {
262651
- headers: { Authorization: `Bearer ${token}` },
262652
- ...signal ? { signal } : {}
262653
- });
262654
- if (!res.ok) {
262655
- const err = await res.text().catch(() => "");
262656
- return { text: `Admin API error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
262657
- }
262658
- const r2 = await res.json();
264166
+ async function fmtGetResource(token, resourceId, signal) {
264167
+ const result = await getResourceRaw(token, resourceId, signal);
264168
+ if (!result.ok) return { text: `Admin API error: ${result.error}`, json: { ok: false, error: result.error } };
264169
+ const r2 = result.data;
262659
264170
  const lines = [
262660
264171
  `Resource: ${r2.resourceName || r2.generatedResourceName || "Unnamed"}`,
262661
264172
  ` ID: ${r2.resourceId}`,
262662
264173
  ` Email: ${r2.resourceEmail || "N/A"}`,
262663
264174
  ` Type: ${r2.resourceType || "N/A"}`,
262664
- typeof r2.capacity === "number" ? ` Capacity: ${r2.capacity}` : "",
264175
+ r2.capacity ? ` Capacity: ${r2.capacity}` : "",
262665
264176
  r2.buildingId ? ` Building: ${r2.buildingId}` : "",
262666
264177
  r2.floorName ? ` Floor: ${r2.floorName}` : "",
262667
264178
  r2.resourceDescription ? ` Description: ${r2.resourceDescription}` : "",
@@ -262669,26 +264180,10 @@ async function getResource(token, resourceId, signal) {
262669
264180
  ].filter(Boolean);
262670
264181
  return { text: lines.join("\n"), json: { ok: true, resource: r2 } };
262671
264182
  }
262672
- async function createResource(token, params, signal) {
262673
- const body = {
262674
- resourceName: params.name,
262675
- resourceType: params.resourceType || "CONFERENCE_ROOM"
262676
- };
262677
- if (params.buildingId) body.buildingId = params.buildingId;
262678
- if (params.capacity) body.capacity = params.capacity;
262679
- if (params.floorName) body.floorName = params.floorName;
262680
- if (params.description) body.resourceDescription = params.description;
262681
- const res = await fetch(`${ADMIN_RESOURCES_BASE}/calendars`, {
262682
- method: "POST",
262683
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
262684
- body: JSON.stringify(body),
262685
- ...signal ? { signal } : {}
262686
- });
262687
- if (!res.ok) {
262688
- const err = await res.text().catch(() => "");
262689
- return { text: `Admin API create error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
262690
- }
262691
- const r2 = await res.json();
264183
+ async function fmtCreateResource(token, params, signal) {
264184
+ const result = await createResourceRaw(token, params, signal);
264185
+ if (!result.ok) return { text: `Admin API create error: ${result.error}`, json: { ok: false, error: result.error } };
264186
+ const r2 = result.data;
262692
264187
  return {
262693
264188
  text: `Created resource: "${r2.resourceName}" (id=${r2.resourceId}, email=${r2.resourceEmail})`,
262694
264189
  json: { ok: true, created: true, resource: r2 }
@@ -262769,9 +264264,9 @@ async function bulkImportResources(token, filePath4, signal, dryRun = false) {
262769
264264
  if (rec.floor || rec.floorName) body.floorName = rec.floor || rec.floorName;
262770
264265
  if (rec.description || rec.resourceDescription) body.resourceDescription = rec.description || rec.resourceDescription;
262771
264266
  try {
262772
- const createRes = await fetch(`${ADMIN_RESOURCES_BASE}/calendars`, {
264267
+ const createRes = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/calendars`, {
262773
264268
  method: "POST",
262774
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
264269
+ headers: { "Content-Type": "application/json" },
262775
264270
  body: JSON.stringify(body),
262776
264271
  ...signal ? { signal } : {}
262777
264272
  });
@@ -262799,54 +264294,25 @@ async function bulkImportResources(token, filePath4, signal, dryRun = false) {
262799
264294
  }
262800
264295
  };
262801
264296
  }
262802
- async function listBuildings(token, signal) {
262803
- const res = await fetch(`${ADMIN_RESOURCES_BASE}/buildings?maxResults=200`, {
262804
- headers: { Authorization: `Bearer ${token}` },
262805
- ...signal ? { signal } : {}
262806
- });
262807
- if (!res.ok) {
262808
- const err = await res.text().catch(() => "");
262809
- return { text: `Admin API error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
262810
- }
262811
- const data = await res.json();
262812
- const buildings = Array.isArray(data.buildings) ? data.buildings : [];
264297
+ async function fmtListBuildings(token, signal) {
264298
+ const result = await listBuildingsRaw(token, signal);
264299
+ if (!result.ok) return { text: `Admin API error: ${result.error}`, json: { ok: false, error: result.error } };
264300
+ const buildings = result.data;
262813
264301
  if (buildings.length === 0) return { text: "No buildings found.", json: { ok: true, buildings: [] } };
262814
264302
  const lines = [`${buildings.length} building(s):`, ""];
262815
264303
  for (const b of buildings) {
262816
264304
  lines.push(` ${b.buildingName || "Unnamed"}`);
262817
264305
  lines.push(` ID: ${b.buildingId}`);
262818
264306
  if (b.description) lines.push(` Description: ${b.description}`);
262819
- if (Array.isArray(b.floorNames) && b.floorNames.length) lines.push(` Floors: ${b.floorNames.join(", ")}`);
264307
+ if (b.floorNames.length) lines.push(` Floors: ${b.floorNames.join(", ")}`);
262820
264308
  lines.push("");
262821
264309
  }
262822
264310
  return { text: lines.join("\n"), json: { ok: true, buildings } };
262823
264311
  }
262824
- async function createBuilding(token, params, signal) {
262825
- const buildingId = params.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `bld-${Date.now()}`;
262826
- const body = {
262827
- buildingId,
262828
- buildingName: params.name
262829
- };
262830
- if (params.description) body.description = params.description;
262831
- if (params.floors) {
262832
- const n = Number(params.floors);
262833
- if (n > 0) {
262834
- body.floorNames = Array.from({ length: n }, (_, i2) => String(i2 + 1));
262835
- } else {
262836
- body.floorNames = params.floors.split(",").map((f3) => f3.trim()).filter(Boolean);
262837
- }
262838
- }
262839
- const res = await fetch(`${ADMIN_RESOURCES_BASE}/buildings`, {
262840
- method: "POST",
262841
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
262842
- body: JSON.stringify(body),
262843
- ...signal ? { signal } : {}
262844
- });
262845
- if (!res.ok) {
262846
- const err = await res.text().catch(() => "");
262847
- return { text: `Admin API create error: ${res.status} ${err.slice(0, 200)}`, json: { ok: false, error: res.status } };
262848
- }
262849
- const b = await res.json();
264312
+ async function fmtCreateBuilding(token, params, signal) {
264313
+ const result = await createBuildingRaw(token, params, signal);
264314
+ if (!result.ok) return { text: `Admin API create error: ${result.error}`, json: { ok: false, error: result.error } };
264315
+ const b = result.data;
262850
264316
  return {
262851
264317
  text: `Created building: "${b.buildingName}" (id=${b.buildingId})`,
262852
264318
  json: { ok: true, created: true, building: b }
@@ -262915,18 +264381,16 @@ async function bulkImportBuildings(token, filePath4, signal, dryRun = false) {
262915
264381
  const rec = records[i2];
262916
264382
  const row = i2 + 2;
262917
264383
  const name = rec.name || rec.buildingName || "";
262918
- const bldId = (rec.id || rec.buildingId || name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || `bld-${Date.now()}`;
264384
+ const bldId = slugify2(rec.id || rec.buildingId || name, "bld");
262919
264385
  const body = { buildingId: bldId, buildingName: name };
262920
264386
  if (rec.description) body.description = rec.description;
262921
264387
  if (rec.floors || rec.floorNames) {
262922
- const f3 = rec.floors || rec.floorNames;
262923
- const n = Number(f3);
262924
- body.floorNames = n > 0 ? Array.from({ length: n }, (_, i22) => String(i22 + 1)) : f3.split(",").map((s2) => s2.trim()).filter(Boolean);
264388
+ body.floorNames = parseFloorNames(rec.floors || rec.floorNames);
262925
264389
  }
262926
264390
  try {
262927
- const createRes = await fetch(`${ADMIN_RESOURCES_BASE}/buildings`, {
264391
+ const createRes = await adminFetch(token, `${ADMIN_RESOURCES_BASE}/buildings`, {
262928
264392
  method: "POST",
262929
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
264393
+ headers: { "Content-Type": "application/json" },
262930
264394
  body: JSON.stringify(body),
262931
264395
  ...signal ? { signal } : {}
262932
264396
  });
@@ -263003,12 +264467,13 @@ async function checkAvailability(token, targets, datetime, durationMinutes, sign
263003
264467
  function createGcalField() {
263004
264468
  return { commandId: "gcal", worker: new GcalWorker(), checker: new AlwaysPassChecker11() };
263005
264469
  }
263006
- var GcalWorker, ADMIN_RESOURCES_BASE, EMAIL_REGEX, AlwaysPassChecker11;
264470
+ var GcalWorker, EMAIL_REGEX, AlwaysPassChecker11;
263007
264471
  var init_gcal_field = __esm({
263008
264472
  "commands/google/gcal.field.ts"() {
263009
264473
  init_base2();
263010
264474
  init_google_oauth();
263011
264475
  init_date_tz();
264476
+ init_admin_resources();
263012
264477
  GcalWorker = class extends LiteWorkerAgent {
263013
264478
  commandId = "gcal";
263014
264479
  help = {
@@ -263116,17 +264581,17 @@ var init_gcal_field = __esm({
263116
264581
  const resSub = (ctx.parsed.args?.[1] || "list").toLowerCase();
263117
264582
  if (resSub === "list") {
263118
264583
  const buildingId = String(ctx.parsed.options.building || "").trim() || void 0;
263119
- return await listResources(token, buildingId, ctx.abortSignal);
264584
+ return await fmtListResources(token, buildingId, ctx.abortSignal);
263120
264585
  }
263121
264586
  if (resSub === "get") {
263122
264587
  const resourceId = String(ctx.parsed.args?.[2] || ctx.parsed.options.id || "").trim();
263123
264588
  if (!resourceId) return { text: "gcal resources get: resourceId is required" };
263124
- return await getResource(token, resourceId, ctx.abortSignal);
264589
+ return await fmtGetResource(token, resourceId, ctx.abortSignal);
263125
264590
  }
263126
264591
  if (resSub === "create") {
263127
264592
  const name = String(ctx.parsed.options.name || "").trim();
263128
264593
  if (!name) return { text: "gcal resources create: --name is required" };
263129
- return await createResource(token, {
264594
+ return await fmtCreateResource(token, {
263130
264595
  name,
263131
264596
  buildingId: String(ctx.parsed.options.building || "").trim() || void 0,
263132
264597
  capacity: Number(ctx.parsed.options.capacity || "0") || void 0,
@@ -263146,12 +264611,12 @@ var init_gcal_field = __esm({
263146
264611
  if (sub === "buildings") {
263147
264612
  const bldSub = (ctx.parsed.args?.[1] || "list").toLowerCase();
263148
264613
  if (bldSub === "list") {
263149
- return await listBuildings(token, ctx.abortSignal);
264614
+ return await fmtListBuildings(token, ctx.abortSignal);
263150
264615
  }
263151
264616
  if (bldSub === "create") {
263152
264617
  const name = String(ctx.parsed.options.name || "").trim();
263153
264618
  if (!name) return { text: "gcal buildings create: --name is required" };
263154
- return await createBuilding(token, {
264619
+ return await fmtCreateBuilding(token, {
263155
264620
  name,
263156
264621
  floors: String(ctx.parsed.options.floors || "").trim() || void 0,
263157
264622
  description: String(ctx.parsed.options.description || "").trim() || void 0
@@ -263180,7 +264645,6 @@ var init_gcal_field = __esm({
263180
264645
  }
263181
264646
  ];
263182
264647
  };
263183
- ADMIN_RESOURCES_BASE = "https://admin.googleapis.com/admin/directory/v1/customer/my_customer/resources";
263184
264648
  EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
263185
264649
  AlwaysPassChecker11 = class extends LiteCheckerAgent {
263186
264650
  commandId = "gcal";
@@ -286736,7 +288200,7 @@ var require_firestore = __commonJS({
286736
288200
  "../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/firestore/index.js"(exports2) {
286737
288201
  Object.defineProperty(exports2, "__esModule", { value: true });
286738
288202
  exports2.FirebaseFirestoreError = exports2.setLogFunction = exports2.v1 = exports2.WriteResult = exports2.WriteBatch = exports2.Transaction = exports2.Timestamp = exports2.QuerySnapshot = exports2.QueryPartition = exports2.QueryDocumentSnapshot = exports2.Query = exports2.GrpcStatus = exports2.GeoPoint = exports2.Firestore = exports2.Filter = exports2.FieldValue = exports2.FieldPath = exports2.DocumentSnapshot = exports2.DocumentReference = exports2.CollectionReference = exports2.CollectionGroup = exports2.BundleBuilder = exports2.BulkWriter = exports2.AggregateQuerySnapshot = exports2.AggregateQuery = exports2.AggregateField = void 0;
286739
- exports2.getFirestore = getFirestore3;
288203
+ exports2.getFirestore = getFirestore4;
286740
288204
  exports2.initializeFirestore = initializeFirestore2;
286741
288205
  var app_1 = require_app();
286742
288206
  var firestore_internal_1 = require_firestore_internal();
@@ -286817,7 +288281,7 @@ var require_firestore = __commonJS({
286817
288281
  Object.defineProperty(exports2, "setLogFunction", { enumerable: true, get: function() {
286818
288282
  return firestore_1.setLogFunction;
286819
288283
  } });
286820
- function getFirestore3(appOrDatabaseId, optionalDatabaseId) {
288284
+ function getFirestore4(appOrDatabaseId, optionalDatabaseId) {
286821
288285
  const app = typeof appOrDatabaseId === "object" ? appOrDatabaseId : (0, app_1.getApp)();
286822
288286
  const databaseId = (typeof appOrDatabaseId === "string" ? appOrDatabaseId : optionalDatabaseId) || path_1.DEFAULT_DATABASE_ID;
286823
288287
  const firebaseApp = app;
@@ -286839,7 +288303,7 @@ var require_firestore = __commonJS({
286839
288303
  });
286840
288304
 
286841
288305
  // ../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/esm/firestore/index.js
286842
- var import_firestore2, getFirestore2;
288306
+ var import_firestore2, getFirestore3;
286843
288307
  var init_firestore = __esm({
286844
288308
  "../node_modules/.pnpm/firebase-admin@13.6.0_encoding@0.1.13/node_modules/firebase-admin/lib/esm/firestore/index.js"() {
286845
288309
  import_firestore2 = __toESM(require_firestore(), 1);
@@ -286867,7 +288331,7 @@ var init_firestore = __esm({
286867
288331
  import_firestore2.default.Transaction;
286868
288332
  import_firestore2.default.WriteBatch;
286869
288333
  import_firestore2.default.WriteResult;
286870
- getFirestore2 = import_firestore2.default.getFirestore;
288334
+ getFirestore3 = import_firestore2.default.getFirestore;
286871
288335
  import_firestore2.default.initializeFirestore;
286872
288336
  import_firestore2.default.setLogFunction;
286873
288337
  import_firestore2.default.v1;
@@ -287173,7 +288637,7 @@ var init_billing_pl_field = __esm({
287173
288637
  json: { error: "access_denied", reason: accessCheck.reason }
287174
288638
  };
287175
288639
  }
287176
- const firestore = getFirestore2();
288640
+ const firestore = getFirestore3();
287177
288641
  const tenantPaths = new TenantPaths(tenantId);
287178
288642
  let targetProjectId = projectId;
287179
288643
  if (isNaturalLanguage && projectNameFromNL && !projectId) {
@@ -287495,7 +288959,7 @@ function createCompetitorsField() {
287495
288959
  const maxRoundsRaw = typeof ctx.parsed.options["max-rounds"] === "string" ? Number(ctx.parsed.options["max-rounds"]) : 10;
287496
288960
  const maxRounds = Number.isFinite(maxRoundsRaw) ? Math.max(1, Math.min(20, Math.floor(maxRoundsRaw))) : 10;
287497
288961
  const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
287498
- const outBaseRel = (outBase || "competitors").replace(/\\/g, "/");
288962
+ const outBaseRel = (outBase || ".maria/desktop/competitors").replace(/\\/g, "/");
287499
288963
  const inOpt = typeof ctx.parsed.options.in === "string" ? String(ctx.parsed.options.in).trim() : "";
287500
288964
  let productDesc = ctx.parsed.args.join(" ").trim();
287501
288965
  if (inOpt) {
@@ -288179,8 +289643,9 @@ function computeWeekWindows(untilDateStr, totalDays) {
288179
289643
  }
288180
289644
  function sparkline(scores, min = 1, max = 5) {
288181
289645
  return scores.map((s2) => {
288182
- const idx = Math.round((s2 - min) / (max - min) * (SPARK_CHARS.length - 1));
288183
- return SPARK_CHARS[Math.max(0, Math.min(SPARK_CHARS.length - 1, idx))];
289646
+ const safe = Number.isFinite(s2) ? s2 : 3;
289647
+ const idx = Math.round((safe - min) / (max - min) * (SPARK_CHARS.length - 1));
289648
+ return SPARK_CHARS[Math.max(0, Math.min(SPARK_CHARS.length - 1, idx))] ?? SPARK_CHARS[4];
288184
289649
  }).join("");
288185
289650
  }
288186
289651
  function computeTrend(scores) {
@@ -288191,7 +289656,7 @@ function computeTrend(scores) {
288191
289656
  let num = 0;
288192
289657
  let den = 0;
288193
289658
  for (let i2 = 0; i2 < n; i2++) {
288194
- num += (i2 - xMean) * ((scores[i2] ?? 3) - yMean);
289659
+ num += (i2 - xMean) * (safeScore(scores[i2] ?? 3) - yMean);
288195
289660
  den += (i2 - xMean) ** 2;
288196
289661
  }
288197
289662
  const slope = den === 0 ? 0 : num / den;
@@ -288209,6 +289674,28 @@ function calcMean(values) {
288209
289674
  if (values.length === 0) return 0;
288210
289675
  return values.reduce((a, b) => a + b, 0) / values.length;
288211
289676
  }
289677
+ function clamp(v, lo, hi) {
289678
+ return Math.min(hi, Math.max(lo, v));
289679
+ }
289680
+ function safeScore(v) {
289681
+ const n = typeof v === "string" ? parseFloat(v) : Number(v);
289682
+ if (!Number.isFinite(n)) return 3;
289683
+ return clamp(n, 1, 5);
289684
+ }
289685
+ function sanitizeSubDimensions(raw, expected) {
289686
+ if (!Array.isArray(raw) || raw.length === 0) {
289687
+ return expected.map((s2) => ({ id: s2.id, score: 3, evidence: ["Parse incomplete"] }));
289688
+ }
289689
+ const lookup = /* @__PURE__ */ new Map();
289690
+ for (const r2 of raw) {
289691
+ if (r2 && typeof r2.id === "string") lookup.set(r2.id.trim().toLowerCase(), r2);
289692
+ }
289693
+ return expected.map((s2) => {
289694
+ const found = lookup.get(s2.id.toLowerCase());
289695
+ if (found) return { id: s2.id, score: safeScore(found.score), evidence: Array.isArray(found.evidence) ? found.evidence : ["(no evidence)"] };
289696
+ return { id: s2.id, score: 3, evidence: ["Sub-dimension not returned by evaluator"] };
289697
+ });
289698
+ }
288212
289699
  function trendEmoji(t2) {
288213
289700
  return t2 === "improving" ? "\u2191" : t2 === "declining" ? "\u2193" : "\u2192";
288214
289701
  }
@@ -288226,7 +289713,7 @@ function computeAllTrends(weeks) {
288226
289713
  const weeklyScores = weeks.map((w) => {
288227
289714
  const de = w.dimensionEvals.find((e2) => e2.dimensionId === dim.id);
288228
289715
  const sd = de?.subDimensions.find((s2) => s2.id === sub.id);
288229
- return sd?.score ?? 3;
289716
+ return safeScore(sd?.score ?? 3);
288230
289717
  });
288231
289718
  trends.push({
288232
289719
  id: sub.id,
@@ -288241,7 +289728,7 @@ function computeAllTrends(weeks) {
288241
289728
  }
288242
289729
  const weeklyOverall = weeks.map((w) => {
288243
289730
  const de = w.dimensionEvals.find((e2) => e2.dimensionId === dim.id);
288244
- return de?.overallScore ?? 3;
289731
+ return safeScore(de?.overallScore ?? 3);
288245
289732
  });
288246
289733
  trends.push({
288247
289734
  id: dim.id,
@@ -288277,7 +289764,7 @@ function computeQuantity(ghData) {
288277
289764
  };
288278
289765
  }
288279
289766
  async function loadPreviousWeekDominants(cwd, username, currentOutDir) {
288280
- const baseDir = path87__namespace.resolve(cwd, "dev-adviser");
289767
+ const baseDir = path87__namespace.resolve(cwd, ".maria/desktop/dev-adviser");
288281
289768
  let entries;
288282
289769
  try {
288283
289770
  entries = await fsp10__namespace.readdir(baseDir);
@@ -288338,6 +289825,75 @@ function neutralWorkClassification() {
288338
289825
  dominantCategory: "new-feature"
288339
289826
  };
288340
289827
  }
289828
+ function validateEvalRaw(raw, numWeeks) {
289829
+ const issues = [];
289830
+ if (!raw || !Array.isArray(raw.weeks)) {
289831
+ issues.push("Top-level 'weeks' array is missing or not an array.");
289832
+ return { ok: false, issues };
289833
+ }
289834
+ if (raw.weeks.length < numWeeks) {
289835
+ issues.push(`Expected ${numWeeks} weeks but got ${raw.weeks.length}.`);
289836
+ }
289837
+ for (let wi = 0; wi < numWeeks; wi++) {
289838
+ const w = raw.weeks[wi];
289839
+ if (!w) {
289840
+ issues.push(`Week ${wi} is missing entirely.`);
289841
+ continue;
289842
+ }
289843
+ if (!w.architectQuality) {
289844
+ issues.push(`Week ${wi}: architectQuality is missing.`);
289845
+ } else {
289846
+ if (!Array.isArray(w.architectQuality.subDimensions) || w.architectQuality.subDimensions.length === 0) {
289847
+ issues.push(`Week ${wi}: architectQuality.subDimensions is empty or missing.`);
289848
+ } else {
289849
+ const returnedIds = new Set(w.architectQuality.subDimensions.map((s2) => String(s2?.id || "")));
289850
+ for (const eid of EXPECTED_AQ_IDS) {
289851
+ if (!returnedIds.has(eid)) issues.push(`Week ${wi}: architectQuality sub-dimension "${eid}" is missing.`);
289852
+ }
289853
+ }
289854
+ if (typeof w.architectQuality.overallScore !== "number") {
289855
+ issues.push(`Week ${wi}: architectQuality.overallScore is not a number.`);
289856
+ }
289857
+ }
289858
+ if (!w.semanticQuality) {
289859
+ issues.push(`Week ${wi}: semanticQuality is missing.`);
289860
+ } else {
289861
+ if (!Array.isArray(w.semanticQuality.subDimensions) || w.semanticQuality.subDimensions.length === 0) {
289862
+ issues.push(`Week ${wi}: semanticQuality.subDimensions is empty or missing.`);
289863
+ } else {
289864
+ const returnedIds = new Set(w.semanticQuality.subDimensions.map((s2) => String(s2?.id || "")));
289865
+ for (const eid of EXPECTED_SQ_IDS) {
289866
+ if (!returnedIds.has(eid)) issues.push(`Week ${wi}: semanticQuality sub-dimension "${eid}" is missing.`);
289867
+ }
289868
+ }
289869
+ if (typeof w.semanticQuality.overallScore !== "number") {
289870
+ issues.push(`Week ${wi}: semanticQuality.overallScore is not a number.`);
289871
+ }
289872
+ }
289873
+ }
289874
+ return { ok: issues.length === 0, issues };
289875
+ }
289876
+ function buildRepairPrompt(brokenText, issues, numWeeks) {
289877
+ const aqIds = ARCHITECT_QUALITY.subDimensions.map((s2) => `"${s2.id}"`).join(", ");
289878
+ const sqIds = SEMANTIC_QUALITY.subDimensions.map((s2) => `"${s2.id}"`).join(", ");
289879
+ return [
289880
+ "MODEL_FIX: Your previous evaluation output is malformed. Repair it and return valid JSON only.",
289881
+ "",
289882
+ "Issues found:",
289883
+ ...issues.map((i2) => ` - ${i2}`),
289884
+ "",
289885
+ `Required: ${numWeeks} weeks, each with architectQuality and semanticQuality.`,
289886
+ `architectQuality sub-dimension IDs: [${aqIds}]`,
289887
+ `semanticQuality sub-dimension IDs: [${sqIds}]`,
289888
+ "Each sub-dimension needs: { id, score (1.0-5.0), evidence (string[]) }",
289889
+ "Each dimension needs: { subDimensions, overallScore (1.0-5.0), summary (string) }",
289890
+ "",
289891
+ "Your broken output (fix this and return correct JSON only):",
289892
+ "```",
289893
+ brokenText.slice(0, 12e3),
289894
+ "```"
289895
+ ].join("\n");
289896
+ }
288341
289897
  async function loadDevAdviserIgnore(cwd) {
288342
289898
  const filePath4 = path87__namespace.resolve(cwd, ".devadviserignore");
288343
289899
  try {
@@ -289080,7 +290636,7 @@ function buildMarkdownReport(result, username, scope) {
289080
290636
  function createDevAdviserField() {
289081
290637
  return { commandId: "dev-adviser", worker: new DevAdviserWorker(), checker: new AlwaysPassChecker20() };
289082
290638
  }
289083
- var BOT_AUTHOR_FILTER, TRAIT_DEFS, TRAIT_THRESHOLD, ARCHITECT_QUALITY, SEMANTIC_QUALITY, ALL_DIMENSIONS, WORK_CATEGORY_LABELS, SPARK_CHARS, FIX_CHASE_PATTERN, MAX_TRIAGE_PER_WEEK, MAX_TRIAGE_PATCH_LINES, MAX_LINES_PER_FILE, SKIP_PATCH_PATTERN, DevAdviserWorker, AlwaysPassChecker20;
290639
+ var BOT_AUTHOR_FILTER, TRAIT_DEFS, TRAIT_THRESHOLD, ARCHITECT_QUALITY, SEMANTIC_QUALITY, ALL_DIMENSIONS, WORK_CATEGORY_LABELS, SPARK_CHARS, MAX_EVAL_REPAIR_ATTEMPTS, EXPECTED_AQ_IDS, EXPECTED_SQ_IDS, FIX_CHASE_PATTERN, MAX_TRIAGE_PER_WEEK, MAX_TRIAGE_PATCH_LINES, MAX_LINES_PER_FILE, SKIP_PATCH_PATTERN, DevAdviserWorker, AlwaysPassChecker20;
289084
290640
  var init_dev_adviser_field = __esm({
289085
290641
  "commands/dev-adviser.field.ts"() {
289086
290642
  init_base2();
@@ -289138,6 +290694,9 @@ var init_dev_adviser_field = __esm({
289138
290694
  "dependency-updates": "Deps"
289139
290695
  };
289140
290696
  SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
290697
+ MAX_EVAL_REPAIR_ATTEMPTS = 2;
290698
+ EXPECTED_AQ_IDS = new Set(ARCHITECT_QUALITY.subDimensions.map((s2) => s2.id));
290699
+ EXPECTED_SQ_IDS = new Set(SEMANTIC_QUALITY.subDimensions.map((s2) => s2.id));
289141
290700
  FIX_CHASE_PATTERN = /\b(fix|bug|patch|hotfix|typo|修正|バグ)\b/i;
289142
290701
  MAX_TRIAGE_PER_WEEK = 30;
289143
290702
  MAX_TRIAGE_PATCH_LINES = 6e3;
@@ -289305,6 +290864,42 @@ Check the username and date range.`
289305
290864
  "",
289306
290865
  patchContext
289307
290866
  ].join("\n");
290867
+ const tryParseEval = (text) => {
290868
+ const rawText = String(text || "");
290869
+ const result2 = parseJsonFromModelText({ text: rawText, label: "dev-adviser.eval" });
290870
+ if (!result2.ok) return { parsed: null, rawText, issues: [`JSON parse failed: ${result2.error.slice(0, 200)}`] };
290871
+ const v = validateEvalRaw(result2.value, numWeeks);
290872
+ if (!v.ok) return { parsed: result2.value, rawText, issues: v.issues };
290873
+ return { parsed: result2.value, rawText, issues: [] };
290874
+ };
290875
+ const evalRawToWeekDimEvals = (raw) => {
290876
+ const result2 = [];
290877
+ for (let wi = 0; wi < numWeeks; wi++) {
290878
+ const wData = raw.weeks[wi];
290879
+ if (wData) {
290880
+ result2.push([
290881
+ {
290882
+ dimensionId: "architect-quality",
290883
+ subDimensions: sanitizeSubDimensions(wData.architectQuality?.subDimensions, ARCHITECT_QUALITY.subDimensions),
290884
+ overallScore: safeScore(wData.architectQuality?.overallScore ?? 3),
290885
+ summary: wData.architectQuality?.summary ?? ""
290886
+ },
290887
+ {
290888
+ dimensionId: "semantic-quality",
290889
+ subDimensions: sanitizeSubDimensions(wData.semanticQuality?.subDimensions, SEMANTIC_QUALITY.subDimensions),
290890
+ overallScore: safeScore(wData.semanticQuality?.overallScore ?? 3),
290891
+ summary: wData.semanticQuality?.summary ?? ""
290892
+ }
290893
+ ]);
290894
+ } else {
290895
+ result2.push([
290896
+ neutralDimensionEval(ARCHITECT_QUALITY, "No data for this week"),
290897
+ neutralDimensionEval(SEMANTIC_QUALITY, "No data for this week")
290898
+ ]);
290899
+ }
290900
+ }
290901
+ return result2;
290902
+ };
289308
290903
  const evalResult = await withLiteSpinner(
289309
290904
  `Evaluating Architect Quality & Semantic Quality (${selectedShas.size} commits examined)...`,
289310
290905
  () => this.aiPromptStructured(ctx, {
@@ -289314,36 +290909,44 @@ Check the username and date range.`
289314
290909
  signal: ctx.abortSignal
289315
290910
  })
289316
290911
  );
289317
- const weekDimensionEvals = [];
290912
+ let weekDimensionEvals = [];
290913
+ let evalAccepted = false;
289318
290914
  if (evalResult.status === "ok") {
289319
- const parsed = parseJsonFromModelText({ text: String(evalResult.text || ""), label: "dev-adviser.eval" });
289320
- if (parsed.ok && Array.isArray(parsed.value.weeks)) {
289321
- for (let wi = 0; wi < numWeeks; wi++) {
289322
- const wData = parsed.value.weeks[wi];
289323
- if (wData) {
289324
- const evals = [];
289325
- evals.push({
289326
- dimensionId: "architect-quality",
289327
- subDimensions: wData.architectQuality?.subDimensions ?? ARCHITECT_QUALITY.subDimensions.map((s2) => ({ id: s2.id, score: 3, evidence: ["Parse incomplete"] })),
289328
- overallScore: wData.architectQuality?.overallScore ?? 3,
289329
- summary: wData.architectQuality?.summary ?? ""
289330
- });
289331
- evals.push({
289332
- dimensionId: "semantic-quality",
289333
- subDimensions: wData.semanticQuality?.subDimensions ?? SEMANTIC_QUALITY.subDimensions.map((s2) => ({ id: s2.id, score: 3, evidence: ["Parse incomplete"] })),
289334
- overallScore: wData.semanticQuality?.overallScore ?? 3,
289335
- summary: wData.semanticQuality?.summary ?? ""
289336
- });
289337
- weekDimensionEvals.push(evals);
289338
- } else {
289339
- weekDimensionEvals.push([
289340
- neutralDimensionEval(ARCHITECT_QUALITY, "No data for this week"),
289341
- neutralDimensionEval(SEMANTIC_QUALITY, "No data for this week")
289342
- ]);
290915
+ const attempt0 = tryParseEval(evalResult.text || "");
290916
+ if (attempt0.issues.length === 0 && attempt0.parsed) {
290917
+ weekDimensionEvals = evalRawToWeekDimEvals(attempt0.parsed);
290918
+ evalAccepted = true;
290919
+ } else {
290920
+ let lastRawText = attempt0.rawText;
290921
+ let lastIssues = attempt0.issues;
290922
+ for (let ri = 1; ri <= MAX_EVAL_REPAIR_ATTEMPTS; ri++) {
290923
+ await emitLog("lite.dev-adviser.eval_repair", { attempt: ri, issues: lastIssues.slice(0, 5) });
290924
+ const repairPrompt = buildRepairPrompt(lastRawText, lastIssues, numWeeks);
290925
+ const repairResult = await withLiteSpinner(
290926
+ `Repairing evaluation format (attempt ${ri}/${MAX_EVAL_REPAIR_ATTEMPTS})...`,
290927
+ () => this.aiPromptStructured(ctx, {
290928
+ taskType: "dev-adviser.eval",
290929
+ systemPrompt: "You are a JSON repair assistant. Fix the broken JSON to match the required schema. Return valid JSON only.",
290930
+ prompt: repairPrompt,
290931
+ signal: ctx.abortSignal
290932
+ })
290933
+ );
290934
+ if (repairResult.status !== "ok") continue;
290935
+ const attemptN = tryParseEval(repairResult.text || "");
290936
+ if (attemptN.issues.length === 0 && attemptN.parsed) {
290937
+ weekDimensionEvals = evalRawToWeekDimEvals(attemptN.parsed);
290938
+ evalAccepted = true;
290939
+ await emitLog("lite.dev-adviser.eval_repair_ok", { attempt: ri });
290940
+ break;
289343
290941
  }
290942
+ lastRawText = attemptN.rawText;
290943
+ lastIssues = attemptN.issues;
289344
290944
  }
289345
290945
  }
289346
290946
  }
290947
+ if (!evalAccepted) {
290948
+ await emitLog("lite.dev-adviser.eval_fallback", { reason: "All eval attempts failed" });
290949
+ }
289347
290950
  while (weekDimensionEvals.length < numWeeks) {
289348
290951
  weekDimensionEvals.push([
289349
290952
  neutralDimensionEval(ARCHITECT_QUALITY, "LLM evaluation unavailable"),
@@ -289382,7 +290985,7 @@ Check the username and date range.`
289382
290985
  }
289383
290986
  const stamp = nowStamp3();
289384
290987
  const titleBase = sanitizeBasename(`dev-assessment-${username}`);
289385
- const outDirRel = outputPath || `dev-adviser/${stamp}_${sanitizeBasename(username)}`;
290988
+ const outDirRel = outputPath || `.maria/desktop/dev-adviser/${stamp}_${sanitizeBasename(username)}`;
289386
290989
  const outDirAbs = path87__namespace.resolve(ctx.cwd, outDirRel);
289387
290990
  const prevDominants = await loadPreviousWeekDominants(ctx.cwd, username, outDirAbs);
289388
290991
  const classificationChangelog = computeClassificationChangelog(weeks, prevDominants);
@@ -289534,6 +291137,69 @@ Check the username and date range.`
289534
291137
  };
289535
291138
  }
289536
291139
  });
291140
+ async function fetchRepoTree(repo, cwd, signal) {
291141
+ const r2 = await runGhCapture({ cwd, signal, args: ["api", `repos/${repo}/git/trees/HEAD?recursive=1`, "--jq", ".tree[] | [.path, .type, (.size // 0)] | @tsv"] });
291142
+ if (r2.exitCode !== 0) return { ok: false, error: r2.stderr.trim() || `gh exit ${r2.exitCode}` };
291143
+ const entries = r2.stdout.trim().split("\n").filter(Boolean).map((line) => {
291144
+ const [p, t2, s2] = line.split(" ");
291145
+ return { path: p ?? "", type: t2 ?? "blob", size: Number(s2 || 0) };
291146
+ });
291147
+ return { ok: true, entries };
291148
+ }
291149
+ function buildCompactTree(entries) {
291150
+ const SKIP = /^(node_modules|\.git|dist|build|\.next|__pycache__|\.cache|vendor)\//;
291151
+ const SKIP_FILES = /\.(lock|min\.js|min\.css|map|snap|svg|png|jpg|jpeg|gif|ico|woff2?|ttf|eot)$/;
291152
+ const filtered = entries.filter((e2) => !SKIP.test(e2.path) && !SKIP_FILES.test(e2.path));
291153
+ return filtered.map((e2) => `${e2.type === "tree" ? "D" : "F"} ${e2.path}`).join("\n");
291154
+ }
291155
+ async function fetchFileContents(repo, paths, cwd, signal) {
291156
+ const MAX_FILES = 20;
291157
+ const MAX_BYTES_PER_FILE = 15e3;
291158
+ const result = /* @__PURE__ */ new Map();
291159
+ const selected = paths.slice(0, MAX_FILES);
291160
+ for (const p of selected) {
291161
+ if (signal.aborted) break;
291162
+ const r2 = await runGhCapture({ cwd, signal, args: ["api", `repos/${repo}/contents/${p}`, "--jq", ".content"] });
291163
+ if (r2.exitCode !== 0) continue;
291164
+ try {
291165
+ const decoded = Buffer.from(r2.stdout.trim(), "base64").toString("utf8");
291166
+ result.set(p, decoded.slice(0, MAX_BYTES_PER_FILE));
291167
+ } catch {
291168
+ }
291169
+ }
291170
+ return result;
291171
+ }
291172
+ async function publishTaskAsIssue(repo, task, projectTitle, cwd, signal) {
291173
+ const body = [
291174
+ `## ${task.id}: ${task.title}`,
291175
+ "",
291176
+ task.description,
291177
+ "",
291178
+ `**Assignee:** ${task.assignee}`,
291179
+ `**Domain:** ${task.domain}`,
291180
+ `**Effort:** Optimistic ${task.effort.optimistic.hours}h / Realistic ${task.effort.realistic.hours}h / Pessimistic ${task.effort.pessimistic.hours}h`,
291181
+ `**Deadline:** ${task.deadline}`,
291182
+ `**Slack:** ${task.slackDays >= 0 ? `${task.slackDays} days` : "unknown"}`,
291183
+ task.onCriticalPath ? "**On Critical Path:** YES" : "",
291184
+ task.dependencies.length > 0 ? `**Dependencies:** ${task.dependencies.join(", ")}` : "",
291185
+ task.relatedFiles.length > 0 ? `
291186
+ **Related Files:**
291187
+ ${task.relatedFiles.map((f3) => `- \`${f3}\``).join("\n")}` : "",
291188
+ "",
291189
+ `**Rationale:** ${task.rationale}`,
291190
+ "",
291191
+ `---`,
291192
+ `*Generated by MARIA OS /task-distribution \u2014 ${projectTitle}*`
291193
+ ].filter(Boolean).join("\n");
291194
+ const r2 = await runGhCapture({
291195
+ cwd,
291196
+ signal,
291197
+ args: ["issue", "create", "--repo", repo, "--title", `[${task.id}] ${task.title}`, "--body", body]
291198
+ });
291199
+ if (r2.exitCode !== 0) return { ok: false, error: r2.stderr.trim() || `gh exit ${r2.exitCode}` };
291200
+ const url = r2.stdout.trim();
291201
+ return { ok: true, url };
291202
+ }
289537
291203
  function renderTaskDistMarkdown(result) {
289538
291204
  const lines = [];
289539
291205
  lines.push(`# ${result.projectTitle}`);
@@ -289627,6 +291293,9 @@ function renderTaskDistMarkdown(result) {
289627
291293
  lines.push(`- **Slack:** ${typeof t2.slackDays === "number" ? `${t2.slackDays} days` : "unknown"}`);
289628
291294
  lines.push(`- **Deadline:** ${t2.deadline}`);
289629
291295
  if (t2.dependencies.length > 0) lines.push(`- **Dependencies:** ${t2.dependencies.join(", ")}`);
291296
+ if (Array.isArray(t2.relatedFiles) && t2.relatedFiles.length > 0) {
291297
+ lines.push(`- **Related Files:** ${t2.relatedFiles.map((f3) => `\`${f3}\``).join(", ")}`);
291298
+ }
289630
291299
  lines.push(`- **Rationale:** ${t2.rationale}`);
289631
291300
  lines.push("");
289632
291301
  }
@@ -289769,13 +291438,17 @@ function createTaskDistributionField() {
289769
291438
  "Examples:",
289770
291439
  ' /task-distribution "Build MVP for customer portal" --members "Alice,Bob,Charlie" --apply',
289771
291440
  ' /task-distribution --in @project-brief.md --members "Dev1,Dev2,Designer,PM" --apply',
291441
+ ' /task-distribution "Add auth module" --members "Dev1,Dev2" --repo "owner/repo" --apply',
291442
+ ' /task-distribution "Refactor payments" --members "A,B" --repo "org/app" --publish-issues --apply',
289772
291443
  "",
289773
291444
  "Options:",
289774
291445
  " --members <list>",
291446
+ " --repo <owner/repo>",
289775
291447
  " --in <file>",
289776
291448
  " --out <dir>",
289777
291449
  " --docx",
289778
291450
  " --pdf",
291451
+ " --publish-issues",
289779
291452
  " --apply",
289780
291453
  " --json"
289781
291454
  ].join("\n")
@@ -289789,8 +291462,10 @@ function createTaskDistributionField() {
289789
291462
  const wantsDocx = hasLiteFlag(ctx.parsed, "docx") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
289790
291463
  const wantsPdf = hasLiteFlag(ctx.parsed, "pdf") || !hasLiteFlag(ctx.parsed, "docx") && !hasLiteFlag(ctx.parsed, "pdf");
289791
291464
  const wantsJson = hasLiteFlag(ctx.parsed, "json");
291465
+ const wantsPublishIssues = hasLiteFlag(ctx.parsed, "publish-issues");
291466
+ const repoSlug = typeof ctx.parsed.options.repo === "string" ? String(ctx.parsed.options.repo).trim() : "";
289792
291467
  const outBase = typeof ctx.parsed.options.out === "string" ? String(ctx.parsed.options.out).trim() : "";
289793
- const outBaseRel = (outBase || "task-distribution").replace(/\\/g, "/");
291468
+ const outBaseRel = (outBase || ".maria/desktop/task-distribution").replace(/\\/g, "/");
289794
291469
  const membersRaw = typeof ctx.parsed.options.members === "string" ? String(ctx.parsed.options.members).trim() : "";
289795
291470
  const members = membersRaw.split(",").map((m2) => m2.trim()).filter(Boolean);
289796
291471
  if (members.length === 0) return { text: `Error: --members is required.
@@ -289812,17 +291487,77 @@ ${this.help.usage}` };
289812
291487
  }
289813
291488
  if (!goal) return { text: `Usage:
289814
291489
  ${this.help.usage}` };
291490
+ let repoContext = "";
291491
+ let repoTreeEntries = [];
291492
+ if (repoSlug) {
291493
+ const ghCheck = await ensureGhInstalled2();
291494
+ if (!ghCheck.ok) return { text: `Error: ${ghCheck.message}
291495
+ --repo requires GitHub CLI.` };
291496
+ const tree = await fetchRepoTree(repoSlug, ctx.cwd, ctx.abortSignal);
291497
+ if (!tree.ok) return { text: `Error fetching repo tree: ${tree.error}` };
291498
+ repoTreeEntries = tree.entries;
291499
+ const compactTree = buildCompactTree(repoTreeEntries);
291500
+ const triagePrompt = [
291501
+ "You are a code-aware task planner. Given a project goal and a repository file tree, select the files most relevant to understanding the codebase for task distribution.",
291502
+ "",
291503
+ `Goal: ${goal}`,
291504
+ additionalContext ? `
291505
+ Additional context:
291506
+ ${additionalContext.slice(0, 1e4)}` : "",
291507
+ "",
291508
+ "## Repository File Tree",
291509
+ compactTree.slice(0, 8e4),
291510
+ "",
291511
+ "Select up to 20 files that are MOST relevant for understanding the codebase structure, architecture, and the areas this goal touches.",
291512
+ "Prefer: entry points, config files, core modules, type definitions, route definitions, schema files.",
291513
+ "Skip: tests, generated files, assets, lock files.",
291514
+ "",
291515
+ "Respond with JSON only:",
291516
+ "```json",
291517
+ '{ "selectedFiles": ["path/to/file1.ts", "path/to/file2.ts", ...],',
291518
+ ' "rationale": "Brief explanation" }',
291519
+ "```"
291520
+ ].join("\n");
291521
+ const triageRes = await this.aiPromptStructured(ctx, {
291522
+ taskType: "chat",
291523
+ prompt: triagePrompt,
291524
+ signal: ctx.abortSignal,
291525
+ spinnerTextOverride: "Analyzing repo structure"
291526
+ });
291527
+ const triageParsed = parseJsonFromModelText({
291528
+ text: String(triageRes.text || ""),
291529
+ label: "repo-triage"
291530
+ });
291531
+ const selectedPaths = triageParsed.ok ? triageParsed.value.selectedFiles || [] : [];
291532
+ const fileContents = selectedPaths.length > 0 ? await fetchFileContents(repoSlug, selectedPaths, ctx.cwd, ctx.abortSignal) : /* @__PURE__ */ new Map();
291533
+ const contextParts = [
291534
+ `
291535
+ ## Repository: ${repoSlug}`,
291536
+ "",
291537
+ "### File Tree (filtered)",
291538
+ compactTree.slice(0, 4e4)
291539
+ ];
291540
+ if (fileContents.size > 0) {
291541
+ contextParts.push("", "### Key File Contents");
291542
+ for (const [fp, content] of fileContents) {
291543
+ contextParts.push("", `#### ${fp}`, "```", content.slice(0, 1e4), "```");
291544
+ }
291545
+ }
291546
+ repoContext = contextParts.join("\n");
291547
+ }
289815
291548
  if (!apply) {
289816
291549
  return {
289817
291550
  text: [
289818
291551
  "Plan: /task-distribution",
289819
291552
  `goal=${goal.slice(0, 120)}${goal.length > 120 ? "..." : ""}`,
289820
291553
  `members=${members.join(", ")}`,
291554
+ repoSlug ? `repo=${repoSlug}` : "",
289821
291555
  `outDir=${outBaseRel}`,
289822
291556
  `docx=${wantsDocx ? "yes" : "no"} pdf=${wantsPdf ? "yes" : "no"}`,
291557
+ wantsPublishIssues ? "publish-issues=yes" : "",
289823
291558
  "",
289824
291559
  "Run with: --apply"
289825
- ].join("\n")
291560
+ ].filter(Boolean).join("\n")
289826
291561
  };
289827
291562
  }
289828
291563
  const stamp = nowStamp3();
@@ -289833,7 +291568,7 @@ ${this.help.usage}` };
289833
291568
  schemaVersion: "maria_lite_task_dist_run_v1",
289834
291569
  runId: ctx.runId,
289835
291570
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
289836
- inputs: { goal: goal.slice(0, 500), members, docx: wantsDocx, pdf: wantsPdf }
291571
+ inputs: { goal: goal.slice(0, 500), members, repo: repoSlug || void 0, docx: wantsDocx, pdf: wantsPdf, publishIssues: wantsPublishIssues }
289837
291572
  });
289838
291573
  const logger = new LiteLogger({ cwd: ctx.cwd });
289839
291574
  const emitLog = async (kind, payload) => {
@@ -289860,7 +291595,7 @@ ${this.help.usage}` };
289860
291595
  `{"schemaVersion":"maria_lite_task_dist_v1","projectTitle":"...","goal":"...","members":["..."],`,
289861
291596
  `"tasks":[{"id":"T-001","title":"...","description":"...","assignee":"member_name","domain":"...","dependencies":["T-xxx"],`,
289862
291597
  `"effort":{"optimistic":{"hours":N,"days":N},"realistic":{"hours":N,"days":N},"pessimistic":{"hours":N,"days":N}},`,
289863
- `"deadline":"YYYY-MM-DD","rationale":"...","slackDays":N,"onCriticalPath":bool}],`,
291598
+ `"deadline":"YYYY-MM-DD","rationale":"...","slackDays":N,"onCriticalPath":bool,"relatedFiles":["path/to/file.ts"]}],`,
289864
291599
  `"interfaces":[{"id":"IF-001","between":["member_A","member_B"],"contract":"...","dataFormat":"...","timing":"...","owner":"manager",`,
289865
291600
  `"boundaryScenarios":["scenario that falls in the gap between both owners' responsibilities"]}],`,
289866
291601
  `"criticalPath":{"path":["T-001","T-003","T-006"],"totalRealisticDays":N,"totalPessimisticDays":N,"bottleneckTaskId":"T-xxx","bottleneckReason":"..."},`,
@@ -289969,6 +291704,16 @@ ${this.help.usage}` };
289969
291704
  " - vulnerabilities: at least 3 structural weaknesses with mitigations.",
289970
291705
  " - idleGaps: per-member idle periods with gap-filling activity recommendations.",
289971
291706
  "",
291707
+ repoSlug ? [
291708
+ "",
291709
+ "11. **Repository-Aware Task Distribution (when repo context is provided):**",
291710
+ " - Use the repository file tree and key file contents to understand the codebase architecture.",
291711
+ " - For each task, populate `relatedFiles` with specific file/directory paths from the repo that the task will touch or depend on.",
291712
+ " - Use actual file paths from the tree \u2014 do NOT invent paths that don't exist in the repo.",
291713
+ " - Task descriptions should reference specific modules, services, and components observed in the codebase.",
291714
+ " - Domain assignments should reflect the actual code organization (e.g., if the repo has `src/auth/`, `src/payments/`, use those as domains)."
291715
+ ].join("\n") : "",
291716
+ "",
289972
291717
  `Today's date: ${todayStr}`,
289973
291718
  `Team members: ${members.join(", ")}`,
289974
291719
  "",
@@ -289977,6 +291722,8 @@ ${this.help.usage}` };
289977
291722
  additionalContext ? `
289978
291723
  Additional context:
289979
291724
  ${additionalContext.slice(0, 6e4)}` : "",
291725
+ repoContext ? `
291726
+ ${repoContext.slice(0, 8e4)}` : "",
289980
291727
  fix ? `
289981
291728
 
289982
291729
  ${fix}
@@ -290007,6 +291754,7 @@ ${fix}
290007
291754
  for (const t2 of v.tasks) {
290008
291755
  if (typeof t2.slackDays !== "number" || !Number.isFinite(t2.slackDays)) t2.slackDays = -1;
290009
291756
  if (typeof t2.onCriticalPath !== "boolean") t2.onCriticalPath = false;
291757
+ if (!Array.isArray(t2.relatedFiles)) t2.relatedFiles = [];
290010
291758
  }
290011
291759
  if (!Array.isArray(v.interfaces)) v.interfaces = [];
290012
291760
  for (const iface of v.interfaces) {
@@ -290081,16 +291829,28 @@ outDir=${outDirRel}` };
290081
291829
  failures.push(`PDF_EXPORT_FAILED: ${msg || "unknown"}`);
290082
291830
  }
290083
291831
  }
291832
+ const issueUrls = [];
291833
+ const issueFailures = [];
291834
+ if (wantsPublishIssues && repoSlug) {
291835
+ for (const task of result.tasks) {
291836
+ const ir = await publishTaskAsIssue(repoSlug, task, result.projectTitle, ctx.cwd, ctx.abortSignal);
291837
+ if (ir.ok && ir.url) issueUrls.push(`${task.id}: ${ir.url}`);
291838
+ else issueFailures.push(`${task.id}: ${ir.error || "unknown"}`);
291839
+ }
291840
+ await emitLog("lite.task_dist.issues", { published: issueUrls.length, failed: issueFailures.length });
291841
+ }
290084
291842
  const ok = failures.length === 0 && ((wantsDocx ? Boolean(exports2.docxRel) : true) && (wantsPdf ? Boolean(exports2.pdfRel) : true));
290085
291843
  await writeJson9(path87__namespace.join(outDirAbs, "summary.json"), {
290086
291844
  schemaVersion: "maria_lite_task_dist_summary_v1",
290087
291845
  ok,
290088
291846
  outDir: outDirRel,
291847
+ repo: repoSlug || void 0,
290089
291848
  taskCount: result.tasks.length,
290090
291849
  interfaceCount: result.interfaces.length,
290091
291850
  memberCount: result.members.length,
290092
291851
  exports: exports2,
290093
- failures
291852
+ failures,
291853
+ issues: issueUrls.length > 0 ? { published: issueUrls, failures: issueFailures } : void 0
290094
291854
  });
290095
291855
  const primaryArtifacts = [];
290096
291856
  if (exports2.docxRel) primaryArtifacts.push({ path: exports2.docxRel, kind: "primary" });
@@ -290108,18 +291868,25 @@ outDir=${outDirRel}` };
290108
291868
  idleGaps: result.idleGaps.length,
290109
291869
  docx: Boolean(exports2.docxRel),
290110
291870
  pdf: Boolean(exports2.pdfRel),
290111
- failures: failures.length
291871
+ failures: failures.length,
291872
+ issuesPublished: issueUrls.length
290112
291873
  });
290113
291874
  const lines = [];
290114
291875
  lines.push(ok ? "OK: /task-distribution" : "WARN: /task-distribution (partial)");
290115
291876
  lines.push(`outDir=${outDirRel}`);
290116
291877
  lines.push(`tasks=${result.tasks.length} members=${result.members.length} interfaces=${result.interfaces.length}`);
291878
+ if (repoSlug) lines.push(`repo=${repoSlug}`);
290117
291879
  if (exports2.docxRel) lines.push(`docx=${exports2.docxRel}`);
290118
291880
  if (exports2.pdfRel) lines.push(`pdf=${exports2.pdfRel}`);
291881
+ if (issueUrls.length > 0) {
291882
+ lines.push(`issues_published=${issueUrls.length}`);
291883
+ for (const u of issueUrls) lines.push(` ${u}`);
291884
+ }
291885
+ if (issueFailures.length > 0) lines.push(`issue_failures=${issueFailures.length}`);
290119
291886
  if (!ok) lines.push(`failures=${failures.length}`);
290120
291887
  return {
290121
291888
  text: lines.join("\n"),
290122
- json: wantsJson ? { ok, outDir: outDirRel, taskCount: result.tasks.length, interfaces: result.interfaces.length, exports: exports2, failures, data: result } : { text: lines.join("\n") },
291889
+ json: wantsJson ? { ok, outDir: outDirRel, repo: repoSlug || void 0, taskCount: result.tasks.length, interfaces: result.interfaces.length, exports: exports2, failures, issues: { published: issueUrls, failures: issueFailures }, data: result } : { text: lines.join("\n") },
290123
291890
  artifacts: [...primaryArtifacts, ...intermediateArtifacts]
290124
291891
  };
290125
291892
  }
@@ -290147,6 +291914,7 @@ var init_task_distribution_field = __esm({
290147
291914
  init_soffice();
290148
291915
  init_slides_helpers();
290149
291916
  init_logger();
291917
+ init_gh_cli();
290150
291918
  }
290151
291919
  });
290152
291920
 
@@ -291460,6 +293228,168 @@ var init_phone_init_field = __esm({
291460
293228
  }
291461
293229
  });
291462
293230
 
293231
+ // commands/resource-manager.field.ts
293232
+ function formatBuildingsTable(items) {
293233
+ const lines = ["## Buildings", ""];
293234
+ lines.push("| ID | Name | Floors | Description |");
293235
+ lines.push("|---|---|---|---|");
293236
+ for (const b of items) {
293237
+ const floors = b.floorNames.join(", ");
293238
+ lines.push(`| ${b.buildingId} | ${b.buildingName} | ${floors} | ${b.description} |`);
293239
+ }
293240
+ lines.push("", `Total: ${items.length} building(s)`);
293241
+ return lines.join("\n");
293242
+ }
293243
+ function formatResourcesTable(items) {
293244
+ const lines = ["## Calendar Resources", ""];
293245
+ lines.push("| Name | Building | Capacity | Type | Floor | Email |");
293246
+ lines.push("|---|---|---|---|---|---|");
293247
+ for (const r2 of items) {
293248
+ lines.push(`| ${r2.resourceName} | ${r2.buildingId} | ${r2.capacity || ""} | ${r2.resourceType} | ${r2.floorName} | ${r2.resourceEmail} |`);
293249
+ }
293250
+ lines.push("", `Total: ${items.length} resource(s)`);
293251
+ return lines.join("\n");
293252
+ }
293253
+ function createResourceManagerField() {
293254
+ const worker = new ResourceManagerWorker();
293255
+ const checker = new ResourceManagerChecker();
293256
+ return { worker, checker };
293257
+ }
293258
+ var ResourceManagerWorker, ResourceManagerChecker;
293259
+ var init_resource_manager_field = __esm({
293260
+ "commands/resource-manager.field.ts"() {
293261
+ init_base2();
293262
+ init_google_oauth();
293263
+ init_admin_resources();
293264
+ ResourceManagerWorker = class extends LiteWorkerAgent {
293265
+ commandId = "resource-manager";
293266
+ help = {
293267
+ command: "/resource-manager",
293268
+ description: "Manage Google Workspace buildings and calendar resources.",
293269
+ usage: '/resource-manager buildings\n/resource-manager resources [--building <buildingId>] [--capacity <n>]\n/resource-manager create-building --name "Main Office" [--floors <n>] [--description <text>]\n/resource-manager create-resource --name "Room A" --building <buildingId> [--capacity <n>] [--type CONFERENCE_ROOM] [--floor <floor>] [--description <text>]\n/resource-manager update-building --id <buildingId> [--name <name>] [--floors <n>] [--description <text>]\n/resource-manager update-resource --id <resourceId> [--name <name>] [--building <buildingId>] [--capacity <n>] [--type <type>] [--floor <floor>] [--description <text>]',
293270
+ runOnEmpty: true
293271
+ };
293272
+ steps = [
293273
+ {
293274
+ id: "resource-manager.exec",
293275
+ title: "Resource Manager",
293276
+ async run(ctx) {
293277
+ const mgr = new GoogleOAuthManager();
293278
+ const token = await mgr.getValidToken(ctx.abortSignal);
293279
+ if (!token) {
293280
+ return {
293281
+ text: "Resource Manager: Google account not connected. Run /google-connect first.",
293282
+ json: { ok: false, error: "not_connected" }
293283
+ };
293284
+ }
293285
+ const sub = (ctx.parsed.subcommand || ctx.parsed.args?.[0] || "buildings").toLowerCase();
293286
+ if (sub === "buildings" || sub === "list-buildings") {
293287
+ const result = await listBuildingsRaw(token, ctx.abortSignal);
293288
+ if (!result.ok) return { text: `Resource Manager: ${result.error}`, json: { ok: false, error: result.error } };
293289
+ const items = result.data;
293290
+ if (items.length === 0) return { text: "No buildings found.", json: { ok: true, buildings: [] } };
293291
+ return { text: formatBuildingsTable(items), json: { ok: true, buildings: items } };
293292
+ }
293293
+ if (sub === "resources" || sub === "list-resources") {
293294
+ const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim() || void 0;
293295
+ const capacityRaw = String(ctx.parsed.options.capacity || "").trim();
293296
+ const capacity = capacityRaw ? Math.max(0, Number(capacityRaw) || 0) : void 0;
293297
+ const result = await listResourcesRaw(token, { buildingId, capacity }, ctx.abortSignal);
293298
+ if (!result.ok) return { text: `Resource Manager: ${result.error}`, json: { ok: false, error: result.error } };
293299
+ const items = result.data;
293300
+ if (items.length === 0) return { text: "No resources found.", json: { ok: true, resources: [] } };
293301
+ return { text: formatResourcesTable(items), json: { ok: true, resources: items } };
293302
+ }
293303
+ if (sub === "create-building") {
293304
+ const name = String(ctx.parsed.options.name || ctx.parsed.prompt || "").trim();
293305
+ if (!name) return { text: "resource-manager create-building: --name is required" };
293306
+ const floorsRaw = String(ctx.parsed.options.floors || "").trim();
293307
+ const description = String(ctx.parsed.options.description || "").trim() || void 0;
293308
+ const result = await createBuildingRaw(token, { name, floors: floorsRaw || void 0, description }, ctx.abortSignal);
293309
+ if (!result.ok) {
293310
+ if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
293311
+ return { text: `Failed to create building: ${result.error}`, json: { ok: false, error: result.error } };
293312
+ }
293313
+ const b = result.data;
293314
+ return { text: `Building created: ${b.buildingName} (${b.buildingId})`, json: { ok: true, building: b } };
293315
+ }
293316
+ if (sub === "create-resource") {
293317
+ const name = String(ctx.parsed.options.name || ctx.parsed.prompt || "").trim();
293318
+ if (!name) return { text: "resource-manager create-resource: --name is required" };
293319
+ const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim() || void 0;
293320
+ const capacity = Number(ctx.parsed.options.capacity || 0) || void 0;
293321
+ const resourceType = String(ctx.parsed.options.type || "CONFERENCE_ROOM").trim();
293322
+ const floorName = String(ctx.parsed.options.floor || "").trim() || void 0;
293323
+ const description = String(ctx.parsed.options.description || "").trim() || void 0;
293324
+ const resourceId = slugify2(name, "res");
293325
+ const result = await createResourceRaw(token, { name, resourceId, buildingId, capacity, resourceType, floorName, description }, ctx.abortSignal);
293326
+ if (!result.ok) {
293327
+ if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
293328
+ return { text: `Failed to create resource: ${result.error}`, json: { ok: false, error: result.error } };
293329
+ }
293330
+ const r2 = result.data;
293331
+ return { text: `Resource created: ${r2.resourceName} (${r2.resourceEmail})`, json: { ok: true, resource: r2 } };
293332
+ }
293333
+ if (sub === "update-building") {
293334
+ const id = String(ctx.parsed.options.id || "").trim();
293335
+ if (!id) return { text: "resource-manager update-building: --id is required" };
293336
+ const body = {};
293337
+ const name = String(ctx.parsed.options.name || "").trim();
293338
+ if (name) body.buildingName = name;
293339
+ const floorsRaw = String(ctx.parsed.options.floors || "").trim();
293340
+ if (floorsRaw) body.floorNames = parseFloorNames(floorsRaw);
293341
+ const description = String(ctx.parsed.options.description || "").trim();
293342
+ if (description) body.description = description;
293343
+ if (Object.keys(body).length === 0) return { text: "resource-manager update-building: at least one field to update is required (--name, --floors, --description)" };
293344
+ const result = await updateBuildingRaw(token, id, body, ctx.abortSignal);
293345
+ if (!result.ok) {
293346
+ if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
293347
+ if (result.error === "not_found") return { text: `Building "${id}" not found.`, json: { ok: false, error: "not_found" } };
293348
+ return { text: `Failed to update building: ${result.error}`, json: { ok: false, error: result.error } };
293349
+ }
293350
+ const b = result.data;
293351
+ return { text: `Building updated: ${b.buildingName} (${b.buildingId})`, json: { ok: true, building: b } };
293352
+ }
293353
+ if (sub === "update-resource") {
293354
+ const id = String(ctx.parsed.options.id || "").trim();
293355
+ if (!id) return { text: "resource-manager update-resource: --id is required" };
293356
+ const body = {};
293357
+ const name = String(ctx.parsed.options.name || "").trim();
293358
+ if (name) body.resourceName = name;
293359
+ const buildingId = String(ctx.parsed.options.building || ctx.parsed.options.buildingId || "").trim();
293360
+ if (buildingId) body.buildingId = buildingId;
293361
+ const capacity = Number(ctx.parsed.options.capacity || 0);
293362
+ if (capacity) body.capacity = capacity;
293363
+ const resourceType = String(ctx.parsed.options.type || "").trim();
293364
+ if (resourceType) body.resourceType = resourceType;
293365
+ const floorName = String(ctx.parsed.options.floor || "").trim();
293366
+ if (floorName) body.floorName = floorName;
293367
+ const description = String(ctx.parsed.options.description || "").trim();
293368
+ if (description) body.resourceDescription = description;
293369
+ if (Object.keys(body).length === 0) return { text: "resource-manager update-resource: at least one field to update is required" };
293370
+ const result = await updateResourceRaw(token, id, body, ctx.abortSignal);
293371
+ if (!result.ok) {
293372
+ if (result.error === "permission_denied") return { text: "Permission denied: Google Workspace admin privileges required.", json: { ok: false, error: "permission_denied" } };
293373
+ if (result.error === "not_found") return { text: `Resource "${id}" not found.`, json: { ok: false, error: "not_found" } };
293374
+ return { text: `Failed to update resource: ${result.error}`, json: { ok: false, error: result.error } };
293375
+ }
293376
+ const r2 = result.data;
293377
+ return { text: `Resource updated: ${r2.resourceName} (${r2.resourceEmail})`, json: { ok: true, resource: r2 } };
293378
+ }
293379
+ return { text: `resource-manager: unknown subcommand "${sub}". Use buildings, resources, create-building, create-resource, update-building, or update-resource.` };
293380
+ }
293381
+ }
293382
+ ];
293383
+ };
293384
+ ResourceManagerChecker = class extends LiteCheckerAgent {
293385
+ commandId = "resource-manager";
293386
+ async check(_ctx, _input) {
293387
+ return { outcome: "PASS", reasons: ["resource_manager_ok"] };
293388
+ }
293389
+ };
293390
+ }
293391
+ });
293392
+
291463
293393
  // runtime/ext/ext-loader.ts
291464
293394
  var ext_loader_exports = {};
291465
293395
  __export(ext_loader_exports, {
@@ -291987,6 +293917,7 @@ function registerCoreLiteCommands(registry) {
291987
293917
  safeRegister(registry, "gcal", createGcalField);
291988
293918
  safeRegister(registry, "gdrive", createGdriveField);
291989
293919
  safeRegister(registry, "gmeet", createGmeetField);
293920
+ safeRegister(registry, "resource-manager", createResourceManagerField);
291990
293921
  }
291991
293922
  function createLiteCommandProviders() {
291992
293923
  const core2 = {
@@ -292116,6 +294047,7 @@ var init_command_providers = __esm({
292116
294047
  init_phone_hp_import_field();
292117
294048
  init_phone_deploy_field();
292118
294049
  init_phone_init_field();
294050
+ init_resource_manager_field();
292119
294051
  init_ext_loader();
292120
294052
  init_beta_loader();
292121
294053
  }