feat: journal dashboard widgets, agent polish, and edit-mode live previews
CI / checks (push) Failing after 2m7s
CI / build (push) Successful in 4m36s

Journal dashboard widgets and quick-add; rich-text quick-add dialogs.

Dashboard draft sync for live edit previews; assistant bubble + API tools.

Journal UX: stress slider, mood grid, query cap fix.
This commit is contained in:
ginnoir
2026-07-04 22:03:45 -05:00
parent 4a924a4107
commit a09747c314
76 changed files with 4594 additions and 611 deletions
+355 -6
View File
@@ -66,6 +66,38 @@ async function dispatchTool(
if (typeof args.qty === "string") body.qty = args.qty;
return callApi(request, origin, "POST", `/api/v1/lists/${listId}/items`, body);
}
case "update_list_item": {
const listId = requireString(args, "listId");
const itemId = requireString(args, "itemId");
const body: Record<string, unknown> = {};
if (typeof args.done === "boolean") body.done = args.done;
if (typeof args.text === "string") body.text = args.text;
if (typeof args.qty === "string") body.qty = args.qty;
return callApi(request, origin, "PATCH", `/api/v1/lists/${listId}/items/${itemId}`, body);
}
case "delete_list_item": {
const listId = requireString(args, "listId");
const itemId = requireString(args, "itemId");
return callApi(request, origin, "DELETE", `/api/v1/lists/${listId}/items/${itemId}`);
}
case "create_list": {
const body = {
type: requireString(args, "type"),
name: requireString(args, "name"),
};
return callApi(request, origin, "POST", "/api/v1/lists", body);
}
case "update_list": {
const listId = requireString(args, "listId");
const body: Record<string, unknown> = {};
if (typeof args.name === "string") body.name = args.name;
if (typeof args.archived === "boolean") body.archived = args.archived;
return callApi(request, origin, "PATCH", `/api/v1/lists/${listId}`, body);
}
case "delete_list": {
const listId = requireString(args, "listId");
return callApi(request, origin, "DELETE", `/api/v1/lists/${listId}`);
}
case "list_calendars":
return callApi(request, origin, "GET", "/api/v1/calendars");
case "list_events": {
@@ -79,29 +111,265 @@ async function dispatchTool(
return callApi(request, origin, "GET", `/api/v1/events?${qs.toString()}`);
}
case "create_event": {
const body = {
const body: Record<string, unknown> = {
calendarId: requireString(args, "calendarId"),
title: requireString(args, "title"),
startAt: requireString(args, "startAt"),
endAt: requireString(args, "endAt"),
allDay: typeof args.allDay === "boolean" ? args.allDay : false,
location: typeof args.location === "string" ? args.location : undefined,
notes: typeof args.notes === "string" ? args.notes : undefined,
};
if (typeof args.location === "string") body.location = args.location;
if (typeof args.notes === "string") body.notes = args.notes;
if (typeof args.remindMinutesBefore === "number") {
body.remindMinutesBefore = args.remindMinutesBefore;
}
return callApi(request, origin, "POST", "/api/v1/events", body);
}
case "update_event": {
const eventId = requireString(args, "eventId");
const body: Record<string, unknown> = {};
if (typeof args.title === "string") body.title = args.title;
if (typeof args.startAt === "string") body.startAt = args.startAt;
if (typeof args.endAt === "string") body.endAt = args.endAt;
if (typeof args.allDay === "boolean") body.allDay = args.allDay;
if (typeof args.location === "string") body.location = args.location;
if (typeof args.notes === "string") body.notes = args.notes;
if (typeof args.remindMinutesBefore === "number") {
body.remindMinutesBefore = args.remindMinutesBefore;
}
return callApi(request, origin, "PATCH", `/api/v1/events/${eventId}`, body);
}
case "delete_event": {
const eventId = requireString(args, "eventId");
return callApi(request, origin, "DELETE", `/api/v1/events/${eventId}`);
}
case "create_calendar": {
const body: Record<string, unknown> = {
name: requireString(args, "name"),
};
if (typeof args.color === "string") body.color = args.color;
if (args.visibility === "private" || args.visibility === "household") {
body.visibility = args.visibility;
}
return callApi(request, origin, "POST", "/api/v1/calendars", body);
}
case "list_notes":
return callApi(request, origin, "GET", "/api/v1/notes");
case "create_note": {
const body = {
const body: Record<string, unknown> = {
title: requireString(args, "title"),
body: typeof args.body === "string" ? args.body : "",
pinned: typeof args.pinned === "boolean" ? args.pinned : false,
};
if (typeof args.remindAt === "string") body.remindAt = args.remindAt;
return callApi(request, origin, "POST", "/api/v1/notes", body);
}
case "list_journal_entries":
return callApi(request, origin, "GET", "/api/v1/journal/entries");
case "update_note": {
const noteId = requireString(args, "noteId");
const body: Record<string, unknown> = {};
if (typeof args.title === "string") body.title = args.title;
if (typeof args.body === "string") body.body = args.body;
if (typeof args.pinned === "boolean") body.pinned = args.pinned;
if (typeof args.remindAt === "string") body.remindAt = args.remindAt;
if (args.remindAt === null) body.remindAt = null;
return callApi(request, origin, "PATCH", `/api/v1/notes/${noteId}`, body);
}
case "delete_note": {
const noteId = requireString(args, "noteId");
return callApi(request, origin, "DELETE", `/api/v1/notes/${noteId}`);
}
case "list_journal_entries": {
const limit = typeof args.limit === "number" ? String(args.limit) : undefined;
const qs = limit ? `?limit=${encodeURIComponent(limit)}` : "";
return callApi(request, origin, "GET", `/api/v1/journal/entries${qs}`);
}
case "create_journal_entry": {
const body: Record<string, unknown> = {
recordedAt: requireString(args, "recordedAt"),
};
if (typeof args.title === "string") body.title = args.title;
if (typeof args.body === "string") body.body = args.body;
if (Array.isArray(args.moods)) body.moods = args.moods;
if (typeof args.stress === "number") body.stress = args.stress;
if (typeof args.pillsTaken === "boolean") body.pillsTaken = args.pillsTaken;
return callApi(request, origin, "POST", "/api/v1/journal/entries", body);
}
case "update_journal_entry": {
const entryId = requireString(args, "entryId");
const body: Record<string, unknown> = {};
if (typeof args.recordedAt === "string") body.recordedAt = args.recordedAt;
if (typeof args.title === "string") body.title = args.title;
if (typeof args.body === "string") body.body = args.body;
if (Array.isArray(args.moods)) body.moods = args.moods;
if (typeof args.stress === "number") body.stress = args.stress;
if (typeof args.pillsTaken === "boolean") body.pillsTaken = args.pillsTaken;
return callApi(request, origin, "PATCH", `/api/v1/journal/entries/${entryId}`, body);
}
case "delete_journal_entry": {
const entryId = requireString(args, "entryId");
return callApi(request, origin, "DELETE", `/api/v1/journal/entries/${entryId}`);
}
case "list_bangs": {
const limit = typeof args.limit === "number" ? String(args.limit) : undefined;
const qs = limit ? `?limit=${encodeURIComponent(limit)}` : "";
return callApi(request, origin, "GET", `/api/v1/bangs${qs}`);
}
case "add_bang": {
const body: Record<string, unknown> = {};
if (typeof args.occurredOn === "string") body.occurredOn = args.occurredOn;
return callApi(request, origin, "POST", "/api/v1/bangs", body);
}
case "update_bang": {
const bangId = requireString(args, "bangId");
const body = { occurredOn: requireString(args, "occurredOn") };
return callApi(request, origin, "PATCH", `/api/v1/bangs/${bangId}`, body);
}
case "delete_bang": {
const bangId = requireString(args, "bangId");
return callApi(request, origin, "DELETE", `/api/v1/bangs/${bangId}`);
}
case "list_garden_containers":
return callApi(request, origin, "GET", "/api/v1/garden/containers");
case "list_garden_plants": {
const containerId = typeof args.containerId === "string" ? args.containerId : undefined;
const qs = containerId ? `?containerId=${encodeURIComponent(containerId)}` : "";
return callApi(request, origin, "GET", `/api/v1/garden/plants${qs}`);
}
case "get_garden_plant": {
const plantId = requireString(args, "plantId");
return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}`);
}
case "create_garden_container": {
const body: Record<string, unknown> = { name: requireString(args, "name") };
if (typeof args.type === "string") body.type = args.type;
if (typeof args.locationNotes === "string") body.locationNotes = args.locationNotes;
return callApi(request, origin, "POST", "/api/v1/garden/containers", body);
}
case "update_garden_container": {
const containerId = requireString(args, "containerId");
const body: Record<string, unknown> = {};
if (typeof args.name === "string") body.name = args.name;
if (typeof args.type === "string") body.type = args.type;
if (typeof args.locationNotes === "string") body.locationNotes = args.locationNotes;
return callApi(request, origin, "PATCH", `/api/v1/garden/containers/${containerId}`, body);
}
case "delete_garden_container": {
const containerId = requireString(args, "containerId");
return callApi(request, origin, "DELETE", `/api/v1/garden/containers/${containerId}`);
}
case "create_garden_plant": {
const body: Record<string, unknown> = { name: requireString(args, "name") };
if (typeof args.category === "string") body.category = args.category;
if (typeof args.containerId === "string") body.containerId = args.containerId;
if (typeof args.healthStatus === "string") body.healthStatus = args.healthStatus;
if (typeof args.notes === "string") body.notes = args.notes;
return callApi(request, origin, "POST", "/api/v1/garden/plants", body);
}
case "update_garden_plant": {
const plantId = requireString(args, "plantId");
const body: Record<string, unknown> = {};
if (typeof args.name === "string") body.name = args.name;
if (typeof args.category === "string") body.category = args.category;
if (typeof args.containerId === "string") body.containerId = args.containerId;
if (typeof args.healthStatus === "string") body.healthStatus = args.healthStatus;
if (typeof args.notes === "string") body.notes = args.notes;
return callApi(request, origin, "PATCH", `/api/v1/garden/plants/${plantId}`, body);
}
case "delete_garden_plant": {
const plantId = requireString(args, "plantId");
return callApi(request, origin, "DELETE", `/api/v1/garden/plants/${plantId}`);
}
case "log_garden_care": {
const plantId = requireString(args, "plantId");
const body: Record<string, unknown> = { careType: requireString(args, "careType") };
if (typeof args.notes === "string") body.notes = args.notes;
if (typeof args.performedAt === "string") body.performedAt = args.performedAt;
return callApi(request, origin, "POST", `/api/v1/garden/plants/${plantId}/care-logs`, body);
}
case "list_garden_care_logs": {
const plantId = requireString(args, "plantId");
const limit = typeof args.limit === "number" ? `?limit=${args.limit}` : "";
return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}/care-logs${limit}`);
}
case "list_garden_care_schedules": {
const plantId = requireString(args, "plantId");
return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}/care-schedules`);
}
case "upsert_garden_care_schedule": {
const plantId = requireString(args, "plantId");
const body: Record<string, unknown> = {
careType: requireString(args, "careType"),
intervalDays: requireNumber(args, "intervalDays"),
};
if (typeof args.enabled === "boolean") body.enabled = args.enabled;
return callApi(
request,
origin,
"POST",
`/api/v1/garden/plants/${plantId}/care-schedules`,
body,
);
}
case "delete_garden_care_schedule": {
const scheduleId = requireString(args, "scheduleId");
return callApi(request, origin, "DELETE", `/api/v1/garden/care-schedules/${scheduleId}`);
}
case "toggle_garden_care_schedule": {
const scheduleId = requireString(args, "scheduleId");
const body = { enabled: requireBoolean(args, "enabled") };
return callApi(request, origin, "PATCH", `/api/v1/garden/care-schedules/${scheduleId}`, body);
}
case "push_overdue_garden_care":
return callApi(request, origin, "POST", "/api/v1/garden/overdue-care/push");
case "schedule_garden_care_on_calendar": {
const scheduleId = requireString(args, "scheduleId");
const body: Record<string, unknown> = {
calendarId: requireString(args, "calendarId"),
};
if (typeof args.reminderMinutesBefore === "number") {
body.reminderMinutesBefore = args.reminderMinutesBefore;
}
return callApi(
request,
origin,
"POST",
`/api/v1/garden/care-schedules/${scheduleId}/calendar`,
body,
);
}
case "list_shareable_entity_types":
return callApi(request, origin, "GET", "/api/v1/share-links?entityTypes=true");
case "list_share_links": {
const qs = new URLSearchParams();
if (typeof args.entityType === "string") qs.set("entityType", args.entityType);
if (typeof args.entityId === "string") qs.set("entityId", args.entityId);
const query = qs.toString();
return callApi(
request,
origin,
"GET",
query ? `/api/v1/share-links?${query}` : "/api/v1/share-links",
);
}
case "create_share_link": {
const body: Record<string, unknown> = {
entityType: requireString(args, "entityType"),
entityId: requireString(args, "entityId"),
};
if (typeof args.expiresAt === "string") body.expiresAt = args.expiresAt;
if (typeof args.write === "boolean") {
body.capabilities = { write: args.write };
}
return callApi(request, origin, "POST", "/api/v1/share-links", body);
}
case "revoke_share_link": {
const linkId = requireString(args, "linkId");
return callApi(request, origin, "DELETE", `/api/v1/share-links/${linkId}`);
}
case "get_api_docs":
return getApiDocs(args);
case "call_api":
return callApiFallback(args, request, origin);
default:
return { status: 400, body: { error: `Unknown tool: ${name}` } };
}
@@ -115,6 +383,87 @@ function requireString(args: Record<string, unknown>, key: string): string {
return value;
}
function requireNumber(args: Record<string, unknown>, key: string): number {
const value = args[key];
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`Missing required argument: ${key}`);
}
return value;
}
function requireBoolean(args: Record<string, unknown>, key: string): boolean {
const value = args[key];
if (typeof value !== "boolean") {
throw new Error(`Missing required argument: ${key}`);
}
return value;
}
async function getApiDocs(args: Record<string, unknown>): Promise<ApiCallResult> {
const { readFile } = await import("fs/promises");
const path = await import("path");
const specPath = path.join(process.cwd(), "docs", "api", "openapi.yaml");
const spec = await readFile(specPath, "utf8");
const search = typeof args.search === "string" ? args.search.trim().toLowerCase() : "";
if (!search) {
return {
status: 200,
body: {
spec,
hint: "Pass search to filter paths, or use call_api with a /api/v1/* path.",
},
};
}
const lines = spec.split("\n");
const matches = lines.filter((line) => line.toLowerCase().includes(search));
return {
status: 200,
body: {
search,
matchCount: matches.length,
matches: matches.slice(0, 100),
hint: "Use call_api with method and path from the matches above.",
},
};
}
async function callApiFallback(
args: Record<string, unknown>,
request: Request,
origin: string,
): Promise<ApiCallResult> {
const method = requireString(args, "method").toUpperCase();
if (!["GET", "POST", "PATCH", "DELETE"].includes(method)) {
return { status: 400, body: { error: "method must be GET, POST, PATCH, or DELETE" } };
}
let apiPath = requireString(args, "path");
if (!apiPath.startsWith("/api/v1/")) {
return { status: 400, body: { error: "path must start with /api/v1/" } };
}
if (apiPath.includes("..")) {
return { status: 400, body: { error: "invalid path" } };
}
if (typeof args.query === "object" && args.query !== null && !Array.isArray(args.query)) {
const qs = new URLSearchParams();
for (const [key, value] of Object.entries(args.query as Record<string, unknown>)) {
if (value !== undefined && value !== null) qs.set(key, String(value));
}
const query = qs.toString();
if (query) apiPath += `?${query}`;
}
const body =
typeof args.body === "object" && args.body !== null && !Array.isArray(args.body)
? (args.body as Record<string, unknown>)
: undefined;
return callApi(request, origin, method, apiPath, body);
}
async function callApi(
request: Request,
origin: string,