feat: journal dashboard widgets, agent polish, and edit-mode live previews
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user