@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
88 lines • 4.2 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { enoent } from "../../errors/fs.js";
import { makeIdName } from "../../utils/naming.js";
import { NAME_MAX_BYTES, byteLength, sanitizeName, stripTrailingUnderscores, truncateBytes, } from "../../utils/sanitize.js";
export const EVENT_SUFFIX = '.gcal.json';
export const CALENDAR_FILE = 'calendar.json';
export const PRIMARY_DIR = 'primary';
// What follows the id: the day a multi-day bucket's names carry, then the
// day-clamped "HHMM-HHMM", then the title when NAME_MAX leaves room for one.
const LABEL_RE = /^(?:([0-9]{4}-[0-9]{2}-[0-9]{2})_)?[0-9]{4}-[0-9]{4}(?:_|$)/;
const UNTITLED = 'untitled';
// A freeBusyReader calendar returns availability with no summary at all, so
// there is no title to sanitize and "busy" is the honest rendering.
const BUSY = 'busy';
/** Pick the title segment for an event filename. */
export function eventTitle(summary, freeBusy = false) {
if (summary !== null && summary.trim() !== '')
return sanitizeName(summary);
return freeBusy ? BUSY : UNTITLED;
}
/**
* Build an event filename: id first, then the day-local times, then title.
*
* The id leads so that trimming the title can never make two events collide
* and so `ls <idprefix>*` addresses one event. Google event ids are 5-1024
* characters by spec (26 in practice), so the title takes whatever of the
* 255-byte NAME_MAX is left rather than a fixed character count. In a
* multi-day bucket the day goes before the times, since the directory no
* longer names it.
*/
export function makeEventFilename(eventId, hhmm, title, day = null) {
const label = day === null ? hhmm : `${day}_${hhmm}`;
const fixed = byteLength(eventId) + 2 + label.length + 1 + EVENT_SUFFIX.length;
const trimmed = stripTrailingUnderscores(truncateBytes(title, NAME_MAX_BYTES - fixed));
if (trimmed === '') {
// The title is what gives, never the id: trimming the id would make the
// name stop addressing the event, which is the whole reason it leads. An
// id long enough that even this form overflows NAME_MAX is therefore
// unnameable rather than silently mangled. The spec permits one (ids run
// 5-1024 chars) but only a caller-supplied id from events.import can be
// that long; Google's own are 26.
return `${eventId}__${label}${EVENT_SUFFIX}`;
}
return `${eventId}__${label}_${trimmed}${EVENT_SUFFIX}`;
}
/**
* Recover `[eventId, day]` from an event filename.
*
* Splitting on the first `__` is safe because a Google event id is base32hex
* and can hold neither an underscore nor a separator, while a title
* routinely holds both. The day is null in a one-day bucket, whose directory
* is the day.
*/
export function parseEventFilename(name) {
if (!name.endsWith(EVENT_SUFFIX))
throw enoent(name);
const raw = name.slice(0, -EVENT_SUFFIX.length);
const idx = raw.indexOf('__');
const label = LABEL_RE.exec(raw.slice(idx + 2));
if (idx <= 0 || label === null)
throw enoent(name);
return [raw.slice(0, idx), label[1] ?? null];
}
/**
* Build the directory name for one calendar.
*
* The primary calendar is spelled `primary` because that is the stable alias
* every Calendar API call accepts, and its summary is only the account's own
* email address.
*/
export function makeCalendarDirname(summary, calendarId, primary = false) {
if (primary)
return PRIMARY_DIR;
return makeIdName(summary, calendarId);
}
//# sourceMappingURL=event_entry.js.map