feat: major platform expansion — Brain service, RSS reader, iOS app, AI assistants, Firefox extension
All checks were successful
Security Checks / dependency-audit (push) Successful in 1m13s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 3s

Brain Service:
- Playwright stealth crawler replacing browserless (og:image, Readability, Reddit JSON API)
- AI classification with tag definitions and folder assignment
- YouTube video download via yt-dlp
- Karakeep migration complete (96 items)
- Taxonomy management (folders with icons/colors, tags)
- Discovery shuffle, sort options, search (Meilisearch + pgvector)
- Item tag/folder editing, card color accents

RSS Reader Service:
- Custom FastAPI reader replacing Miniflux
- Feed management (add/delete/refresh), category support
- Full article extraction via Readability
- Background content fetching for new entries
- Mark all read with confirmation
- Infinite scroll, retention cleanup (30/60 day)
- 17 feeds migrated from Miniflux

iOS App (SwiftUI):
- Native iOS 17+ app with @Observable architecture
- Cookie-based auth, configurable gateway URL
- Dashboard with custom background photo + frosted glass widgets
- Full fitness module (today/templates/goals/food library)
- AI assistant chat (fitness + brain, raw JSON state management)
- 120fps ProMotion support

AI Assistants (Gateway):
- Unified dispatcher with fitness/brain domain detection
- Fitness: natural language food logging, photo analysis, multi-item splitting
- Brain: save/append/update/delete notes, search & answer, undo support
- Madiha user gets fitness-only (brain disabled)

Firefox Extension:
- One-click save to Brain from any page
- Login with platform credentials
- Right-click context menu (save page/link/image)
- Notes field for URL saves
- Signed and published on AMO

Other:
- Reader bookmark button routes to Brain (was Karakeep)
- Fitness food library with "Add" button + add-to-meal popup
- Kindle send file size check (25MB SMTP2GO limit)
- Atelier UI as default (useAtelierShell=true)
- Mobile upload box in nav drawer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 00:56:29 -05:00
parent af1765bd8e
commit 4592e35732
97 changed files with 11009 additions and 532 deletions

View File

@@ -6,7 +6,7 @@ const gatewayUrl = env.GATEWAY_URL || 'http://localhost:8100';
export const load: LayoutServerLoad = async ({ cookies, url }) => {
const host = url.host.toLowerCase();
const useAtelierShell = host.includes(':4174') || host.startsWith('test.');
const useAtelierShell = true;
const session = cookies.get('platform_session');
if (!session) {
throw redirect(302, `/login?redirect=${encodeURIComponent(url.pathname)}`);
@@ -26,7 +26,7 @@ export const load: LayoutServerLoad = async ({ cookies, url }) => {
// Hiding reduces clutter for users who don't need certain apps day-to-day.
const allApps = ['tasks', 'trips', 'fitness', 'inventory', 'budget', 'reader', 'media', 'brain'];
const hiddenByUser: Record<string, string[]> = {
'madiha': ['inventory', 'reader'],
'madiha': ['inventory', 'reader', 'brain'],
};
const hidden = hiddenByUser[data.user.username] || [];
const visibleApps = allApps.filter(a => !hidden.includes(a));

View File

@@ -10,6 +10,7 @@
let assistantEntryDate = $state<string | null>(null);
const visibleApps = data?.visibleApps || ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media'];
const userName = data?.user?.display_name || '';
const assistantBrainEnabled = data?.user?.username !== 'madiha';
const useAtelierShell = data?.useAtelierShell || false;
function openCommand() {
@@ -40,7 +41,7 @@
<AppShell onOpenCommand={openCommand} {visibleApps} {userName}>
{@render children()}
</AppShell>
<FitnessAssistantDrawer bind:open={commandOpen} onclose={closeCommand} entryDate={assistantEntryDate} />
<FitnessAssistantDrawer bind:open={commandOpen} onclose={closeCommand} entryDate={assistantEntryDate} allowBrain={assistantBrainEnabled} />
{:else}
<div class="app">
<Navbar onOpenCommand={openCommand} {visibleApps} />

View File

@@ -0,0 +1,140 @@
import { json } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
type ChatRole = 'user' | 'assistant';
type ChatMessage = {
role?: ChatRole;
content?: string;
};
type UnifiedState = {
activeDomain?: 'fitness' | 'brain';
fitnessState?: Record<string, unknown>;
brainState?: Record<string, unknown>;
};
function recentMessages(messages: unknown): Array<{ role: ChatRole; content: string }> {
if (!Array.isArray(messages)) return [];
return messages
.filter((message) => !!message && typeof message === 'object')
.map((message) => {
const item = message as ChatMessage;
return {
role: item.role === 'assistant' ? 'assistant' : 'user',
content: typeof item.content === 'string' ? item.content : ''
};
})
.filter((message) => message.content.trim())
.slice(-16);
}
function lastUserMessage(messages: Array<{ role: ChatRole; content: string }>): string {
return [...messages].reverse().find((message) => message.role === 'user')?.content?.trim() || '';
}
function isFitnessIntent(text: string): boolean {
return /\b(calories?|protein|carbs?|fat|sugar|fiber|breakfast|lunch|dinner|snack|meal|food|ate|eaten|log|track|entries|macros?)\b/i.test(text)
|| /\bfor (breakfast|lunch|dinner|snack)\b/i.test(text)
|| /\bhow many calories do i have left\b/i.test(text);
}
function isBrainIntent(text: string): boolean {
return /\b(note|notes|brain|remember|save this|save that|what do i have|what have i saved|find my|delete (?:that|this|note|item)|update (?:that|this|note)|from my notes)\b/i.test(text);
}
function detectDomain(
messages: Array<{ role: ChatRole; content: string }>,
state: UnifiedState,
imageDataUrl?: string | null
): 'fitness' | 'brain' {
const text = lastUserMessage(messages);
if (imageDataUrl) return 'fitness';
if (!text) return state.activeDomain || 'brain';
const fitness = isFitnessIntent(text);
const brain = isBrainIntent(text);
if (fitness && !brain) return 'fitness';
if (brain && !fitness) return 'brain';
if (state.activeDomain === 'fitness' && !brain) return 'fitness';
if (state.activeDomain === 'brain' && !fitness) return 'brain';
return fitness ? 'fitness' : 'brain';
}
export const POST: RequestHandler = async ({ request, fetch, cookies }) => {
if (!cookies.get('platform_session')) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const { messages = [], state = {}, imageDataUrl = null, entryDate = null, action = 'chat', allowBrain = true } = await request
.json()
.catch(() => ({}));
let brainEnabled = !!allowBrain;
try {
const gatewayUrl = env.GATEWAY_URL || 'http://localhost:8100';
const session = cookies.get('platform_session');
if (session) {
const auth = await fetch(`${gatewayUrl}/api/auth/me`, {
headers: { Cookie: `platform_session=${session}` }
});
if (auth.ok) {
const data = await auth.json().catch(() => null);
if (data?.authenticated && data?.user?.username === 'madiha') {
brainEnabled = false;
}
}
}
} catch {
// keep requested allowBrain fallback
}
const chat = recentMessages(messages);
const unifiedState: UnifiedState = state && typeof state === 'object' ? state : {};
const domain = brainEnabled ? detectDomain(chat, unifiedState, imageDataUrl) : 'fitness';
const response = await fetch(domain === 'fitness' ? '/assistant/fitness' : '/assistant/brain', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(
domain === 'fitness'
? {
action,
messages,
draft: unifiedState.fitnessState && 'draft' in unifiedState.fitnessState ? unifiedState.fitnessState.draft : null,
drafts: unifiedState.fitnessState && 'drafts' in unifiedState.fitnessState ? unifiedState.fitnessState.drafts : [],
entryDate,
imageDataUrl
}
: {
messages,
state: unifiedState.brainState || {}
}
)
});
const body = await response.json().catch(() => ({}));
if (!response.ok) {
return json(body, { status: response.status });
}
return json({
...body,
domain,
state: {
activeDomain: domain,
fitnessState:
domain === 'fitness'
? {
draft: body?.draft ?? null,
drafts: Array.isArray(body?.drafts) ? body.drafts : []
}
: unifiedState.fitnessState || {},
brainState: domain === 'brain' ? body?.state || {} : unifiedState.brainState || {}
}
});
};

View File

@@ -0,0 +1,799 @@
import { env } from '$env/dynamic/private';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
type ChatRole = 'user' | 'assistant';
type ChatMessage = {
role?: ChatRole;
content?: string;
};
type BrainItem = {
id: string;
type: string;
title?: string | null;
url?: string | null;
raw_content?: string | null;
extracted_text?: string | null;
folder?: string | null;
tags?: string[] | null;
summary?: string | null;
created_at?: string;
};
type BrainAddition = {
id: string;
item_id: string;
content: string;
created_at: string;
};
type AssistantState = {
lastMutation?: {
type: 'append' | 'create' | 'update';
itemId: string;
itemTitle: string;
additionId?: string;
content: string;
createdItemId?: string;
previousRawContent?: string;
};
pendingDelete?: {
itemId: string;
itemTitle: string;
};
};
type SourceLink = {
id: string;
title: string;
type: string;
href: string;
};
type SearchQueryDecision = {
queries?: string[];
};
function recentMessages(messages: unknown): Array<{ role: ChatRole; content: string }> {
if (!Array.isArray(messages)) return [];
return messages
.filter((m) => !!m && typeof m === 'object')
.map((m) => {
const message = m as ChatMessage;
return {
role: (message.role === 'assistant' ? 'assistant' : 'user') as ChatRole,
content: typeof message.content === 'string' ? message.content.slice(0, 4000) : ''
};
})
.filter((m) => m.content.trim())
.slice(-12);
}
function lastUserMessage(messages: Array<{ role: ChatRole; content: string }>): string {
return [...messages].reverse().find((message) => message.role === 'user')?.content?.trim() || '';
}
function toSource(item: BrainItem): SourceLink {
return {
id: item.id,
title: item.title || 'Untitled',
type: item.type,
href: `/brain?item=${item.id}`
};
}
function isConfirmation(text: string): boolean {
const clean = text.trim().toLowerCase();
return [
'yes',
'yes delete it',
'delete it',
'confirm',
'yes do it',
'do it',
'go ahead'
].includes(clean);
}
function isUndo(text: string): boolean {
return /^(undo|undo last change|undo that|revert that)$/i.test(text.trim());
}
function wantsNewNoteInstead(text: string): boolean {
return /create (?:a )?new note/i.test(text) || /make (?:that|it) a new note/i.test(text);
}
function moveTargetFromText(text: string): string | null {
const match = text.match(/(?:add|move)\s+(?:that|it)\s+to\s+(.+)$/i);
return match?.[1]?.trim() || null;
}
function wantsDelete(text: string): boolean {
return /\bdelete\b/i.test(text) && /\b(note|item|that)\b/i.test(text);
}
function isExplicitUpdateRequest(text: string): boolean {
return /\b(update|edit|change|replace|correct|set)\b/i.test(text);
}
function isListingIntent(text: string): boolean {
return /\b(what|which|show|list)\b/i.test(text)
&& /\b(do i have|have i saved|saved|notes|items|books?|pdfs?|links?|documents?|files?)\b/i.test(text);
}
function buildCandidateSummary(items: BrainItem[]): string {
if (!items.length) return 'No candidates found.';
return items
.map((item, index) => {
const snippet = (item.raw_content || item.extracted_text || item.summary || '')
.replace(/\s+/g, ' ')
.slice(0, 220);
return `${index + 1}. id=${item.id}
title=${item.title || 'Untitled'}
type=${item.type}
folder=${item.folder || ''}
tags=${(item.tags || []).join(', ')}
snippet=${snippet}`;
})
.join('\n\n');
}
async function brainSearch(fetchFn: typeof fetch, q: string, limit = 5): Promise<BrainItem[]> {
const response = await fetchFn('/api/brain/search/hybrid', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ q, limit })
});
if (!response.ok) return [];
const body = await response.json().catch(() => ({}));
return Array.isArray(body?.items) ? body.items : [];
}
function normalizeSearchSeed(text: string): string {
return text
.trim()
.replace(/^what\s+(books?|notes?|pdfs?|links?|documents?|files?)\s+do i have saved\s*/i, '$1 ')
.replace(/^show me\s+(all\s+)?(books?|notes?|pdfs?|links?|documents?|files?)\s*/i, '$2 ')
.replace(/^list\s+(my\s+)?(books?|notes?|pdfs?|links?|documents?|files?)\s*/i, '$2 ')
.replace(/^add note\s+/i, '')
.replace(/^add\s+/i, '')
.replace(/^save\s+(?:this|that)\s+/i, '')
.replace(/^what do i have about\s+/i, '')
.replace(/^what have i saved about\s+/i, '')
.replace(/^find\s+/i, '')
.replace(/^search for\s+/i, '')
.replace(/^answer\s+/i, '')
.trim();
}
async function deriveSearchQueries(
messages: Array<{ role: ChatRole; content: string }>,
userText: string
): Promise<string[]> {
const normalized = normalizeSearchSeed(userText);
const fallback = [normalized || userText.trim()].filter(Boolean);
if (!env.OPENAI_API_KEY) return fallback;
const systemPrompt = `You extract concise retrieval queries for a personal knowledge base.
Return ONLY JSON:
{
"queries": ["query one", "query two"]
}
Rules:
- Return 1 to 3 short search queries.
- Focus on the underlying topic, not chat filler.
- For "what do I have about X", include just X.
- For advice/tips, you may infer a likely broader topic if strongly implied.
- Keep each query short, usually 1 to 5 words.
- Do not include punctuation-heavy sentences.
- Do not include explanatory text.`;
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: env.OPENAI_MODEL || 'gpt-5.2',
response_format: { type: 'json_object' },
temperature: 0.1,
max_completion_tokens: 200,
messages: [
{ role: 'system', content: systemPrompt },
...messages.slice(-6),
{ role: 'user', content: `Search intent: ${userText}` }
]
})
});
if (!response.ok) return fallback;
const raw = await response.json().catch(() => null);
const content = raw?.choices?.[0]?.message?.content;
if (typeof content !== 'string') return fallback;
try {
const parsed = JSON.parse(content) as SearchQueryDecision;
const queries = Array.isArray(parsed.queries)
? parsed.queries.map((query) => String(query || '').trim()).filter(Boolean).slice(0, 3)
: [];
return queries.length ? queries : fallback;
} catch {
return fallback;
}
}
async function collectCandidates(
fetchFn: typeof fetch,
messages: Array<{ role: ChatRole; content: string }>,
userText: string,
limit = 8
): Promise<BrainItem[]> {
const queries = await deriveSearchQueries(messages, userText);
const merged = new Map<string, BrainItem>();
for (const query of queries) {
const items = await brainSearch(fetchFn, query, limit);
for (const item of items) {
if (!merged.has(item.id)) merged.set(item.id, item);
}
}
if (!merged.size) {
for (const item of await brainSearch(fetchFn, userText, limit)) {
if (!merged.has(item.id)) merged.set(item.id, item);
}
}
return [...merged.values()].slice(0, limit);
}
async function createBrainNote(fetchFn: typeof fetch, content: string, title?: string) {
const response = await fetchFn('/api/brain/items', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
type: 'note',
raw_content: content,
title: title?.trim() || undefined
})
});
return {
ok: response.ok,
status: response.status,
body: await response.json().catch(() => ({}))
};
}
async function updateBrainItem(fetchFn: typeof fetch, itemId: string, body: { raw_content?: string; title?: string }) {
const response = await fetchFn(`/api/brain/items/${itemId}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
return {
ok: response.ok,
status: response.status,
body: await response.json().catch(() => ({}))
};
}
async function appendToItem(fetchFn: typeof fetch, itemId: string, content: string) {
const response = await fetchFn(`/api/brain/items/${itemId}/additions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ content, source: 'assistant', kind: 'append' })
});
return {
ok: response.ok,
status: response.status,
body: await response.json().catch(() => ({}))
};
}
async function deleteAddition(fetchFn: typeof fetch, itemId: string, additionId: string) {
const response = await fetchFn(`/api/brain/items/${itemId}/additions/${additionId}`, {
method: 'DELETE'
});
return response.ok;
}
async function deleteItem(fetchFn: typeof fetch, itemId: string) {
const response = await fetchFn(`/api/brain/items/${itemId}`, { method: 'DELETE' });
return response.ok;
}
async function getItem(fetchFn: typeof fetch, itemId: string): Promise<BrainItem | null> {
const response = await fetchFn(`/api/brain/items/${itemId}`);
if (!response.ok) return null;
return response.json().catch(() => null);
}
async function decideAction(
messages: Array<{ role: ChatRole; content: string }>,
candidates: BrainItem[],
state: AssistantState,
listingIntent = false
) {
const systemPrompt = `You are the Brain assistant inside a personal app.
You help the user save notes naturally, append thoughts to existing items, answer questions from saved notes, and choose whether to create a new note.
Return ONLY JSON with this shape:
{
"action": "append_existing" | "create_new_note" | "update_existing" | "answer" | "list_items" | "delete_target",
"reply": "short reply preview",
"target_item_id": "optional item id",
"formatted_content": "text to append or create",
"create_title": "short AI-generated title when creating a new note",
"answer": "short answer when action=answer",
"source_item_ids": ["id1", "id2"],
"match_confidence": "high" | "low"
}
Rules:
- Use existing items only when the topical match is strong.
- If the match is weak or ambiguous, create a new note.
- The user prefers speed: do not ask which note to use.
- If the user explicitly says update, change, edit, replace, set, or correct, prefer update_existing when one strong existing note clearly matches.
- If the user is asking to list what they have saved, prefer list_items instead of answer.
- For short tips/advice, format as bullets when natural.
- Only fix spelling and grammar. Do not rewrite the meaning.
- For questions, answer briefly and cite up to 3 source ids.
- For list_items, return a concise list-style reply and cite up to 12 source ids.
- For delete requests, choose the most likely target and set action=delete_target.
- Never choose append_existing unless target_item_id is one of the candidate ids.
- Never choose update_existing unless target_item_id is one of the candidate ids.
- If all candidates are weak, set action=create_new_note and match_confidence=low.
- The current assistant state is only for context:
${JSON.stringify(state, null, 2)}
- listing_intent=${listingIntent}
`;
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: env.OPENAI_MODEL || 'gpt-5.2',
response_format: { type: 'json_object' },
temperature: 0.2,
max_completion_tokens: 1100,
messages: [
{ role: 'system', content: systemPrompt },
...messages,
{
role: 'user',
content: `Candidate items:\n${buildCandidateSummary(candidates)}`
}
]
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const raw = await response.json();
const content = raw?.choices?.[0]?.message?.content;
if (typeof content !== 'string') {
throw new Error('Assistant response was empty.');
}
return JSON.parse(content) as {
action?: 'append_existing' | 'create_new_note' | 'update_existing' | 'answer' | 'list_items' | 'delete_target';
reply?: string;
target_item_id?: string;
formatted_content?: string;
create_title?: string;
answer?: string;
source_item_ids?: string[];
match_confidence?: 'high' | 'low';
};
}
async function rewriteExistingItemContent(
messages: Array<{ role: ChatRole; content: string }>,
target: BrainItem,
userInstruction: string
): Promise<string> {
const existing = (target.raw_content || target.extracted_text || '').trim();
if (!existing) {
throw new Error('Target item has no editable content.');
}
const systemPrompt = `You edit an existing personal note in place.
Return ONLY JSON:
{
"updated_content": "full updated note body"
}
Rules:
- Apply the user's requested update to the existing note.
- Preserve the note's structure and wording as much as possible.
- Make the smallest correct change needed.
- Do not append a new line when the user clearly wants an existing value updated.
- Only fix spelling and grammar where needed for the changed text.
- Return the full updated note body, not a diff.`;
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: env.OPENAI_MODEL || 'gpt-5.2',
response_format: { type: 'json_object' },
temperature: 0.1,
max_completion_tokens: 900,
messages: [
{ role: 'system', content: systemPrompt },
...messages.slice(-8),
{
role: 'user',
content: `Target title: ${target.title || 'Untitled'}\n\nExisting note:\n${existing}\n\nInstruction: ${userInstruction}`
}
]
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const raw = await response.json();
const content = raw?.choices?.[0]?.message?.content;
if (typeof content !== 'string') {
throw new Error('Update response was empty.');
}
const parsed = JSON.parse(content) as { updated_content?: string };
const updated = parsed.updated_content?.trim();
if (!updated) {
throw new Error('Updated content was empty.');
}
return updated;
}
function sourceLinksFromIds(items: BrainItem[], ids: string[] | undefined): SourceLink[] {
if (!Array.isArray(ids) || !ids.length) return [];
const map = new Map(items.map((item) => [item.id, item]));
return ids.map((id) => map.get(id)).filter(Boolean).map((item) => toSource(item as BrainItem));
}
export const POST: RequestHandler = async ({ request, fetch, cookies }) => {
if (!cookies.get('platform_session')) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
if (!env.OPENAI_API_KEY) {
return json({ error: 'Assistant is not configured.' }, { status: 500 });
}
const { messages = [], state = {} } = await request.json().catch(() => ({}));
const chat = recentMessages(messages);
const userText = lastUserMessage(chat);
const currentState: AssistantState = state && typeof state === 'object' ? state : {};
if (!userText) {
return json({
reply: 'Tell me what to save, find, answer, or delete.',
state: currentState,
sources: []
});
}
if (currentState.pendingDelete && isConfirmation(userText)) {
const target = currentState.pendingDelete;
const ok = await deleteItem(fetch, target.itemId);
if (!ok) {
return json({ reply: `I couldn't delete "${target.itemTitle}" yet.`, state: currentState, sources: [] }, { status: 500 });
}
return json({
reply: `Deleted "${target.itemTitle}".`,
state: {},
sources: []
});
}
if (currentState.lastMutation && isUndo(userText)) {
if (currentState.lastMutation.type === 'append' && currentState.lastMutation.additionId) {
const ok = await deleteAddition(fetch, currentState.lastMutation.itemId, currentState.lastMutation.additionId);
return json({
reply: ok ? `Undid the change in "${currentState.lastMutation.itemTitle}".` : 'I could not undo that change yet.',
state: ok ? {} : currentState,
sources: ok ? [{ id: currentState.lastMutation.itemId, title: currentState.lastMutation.itemTitle, type: 'note', href: `/brain?item=${currentState.lastMutation.itemId}` }] : []
});
}
if (currentState.lastMutation.type === 'create' && currentState.lastMutation.createdItemId) {
const ok = await deleteItem(fetch, currentState.lastMutation.createdItemId);
return json({
reply: ok ? `Removed "${currentState.lastMutation.itemTitle}".` : 'I could not undo that note creation yet.',
state: ok ? {} : currentState,
sources: []
});
}
if (currentState.lastMutation.type === 'update' && currentState.lastMutation.previousRawContent !== undefined) {
const restored = await updateBrainItem(fetch, currentState.lastMutation.itemId, {
raw_content: currentState.lastMutation.previousRawContent
});
return json({
reply: restored.ok ? `Undid the update in "${currentState.lastMutation.itemTitle}".` : 'I could not undo that update yet.',
state: restored.ok ? {} : currentState,
sources: restored.ok ? [{ id: currentState.lastMutation.itemId, title: currentState.lastMutation.itemTitle, type: 'note', href: `/brain?item=${currentState.lastMutation.itemId}` }] : []
});
}
}
if (currentState.lastMutation && wantsNewNoteInstead(userText)) {
const content = currentState.lastMutation.content;
if (currentState.lastMutation.type === 'append' && currentState.lastMutation.additionId) {
await deleteAddition(fetch, currentState.lastMutation.itemId, currentState.lastMutation.additionId);
}
const titleDecision = await decideAction(
[{ role: 'user', content: `Create a concise title for this note:\n${content}` }],
[],
currentState
).catch(() => ({ create_title: 'New Note' }));
const created = await createBrainNote(fetch, content, titleDecision.create_title);
if (!created.ok) {
return json({ reply: 'I could not create the new note yet.', state: currentState, sources: [] }, { status: 500 });
}
const createdItem = created.body as BrainItem;
return json({
reply: `Created "${createdItem.title || titleDecision.create_title || 'New note'}".`,
state: {
lastMutation: {
type: 'create',
itemId: createdItem.id,
createdItemId: createdItem.id,
itemTitle: createdItem.title || titleDecision.create_title || 'New note',
content
}
},
sources: [toSource(createdItem)]
});
}
const retarget = currentState.lastMutation ? moveTargetFromText(userText) : null;
if (currentState.lastMutation && retarget) {
if (currentState.lastMutation.type === 'append' && currentState.lastMutation.additionId) {
await deleteAddition(fetch, currentState.lastMutation.itemId, currentState.lastMutation.additionId);
} else if (currentState.lastMutation.type === 'create' && currentState.lastMutation.createdItemId) {
await deleteItem(fetch, currentState.lastMutation.createdItemId);
}
const candidates = await collectCandidates(fetch, chat, retarget, 8);
const target = candidates[0];
if (!target) {
const created = await createBrainNote(fetch, currentState.lastMutation.content, retarget);
if (!created.ok) {
return json({ reply: 'I could not move that into a new note yet.', state: currentState, sources: [] }, { status: 500 });
}
const createdItem = created.body as BrainItem;
return json({
reply: `Created "${createdItem.title || retarget}".`,
state: {
lastMutation: {
type: 'create',
itemId: createdItem.id,
createdItemId: createdItem.id,
itemTitle: createdItem.title || retarget,
content: currentState.lastMutation.content
}
},
sources: [toSource(createdItem)]
});
}
const appended = await appendToItem(fetch, target.id, currentState.lastMutation.content);
if (!appended.ok) {
return json({ reply: `I couldn't move that to "${target.title || 'that item'}" yet.`, state: currentState, sources: [] }, { status: 500 });
}
return json({
reply: `Moved it to "${target.title || 'Untitled'}".`,
state: {
lastMutation: {
type: 'append',
itemId: target.id,
itemTitle: target.title || 'Untitled',
additionId: appended.body?.id,
content: currentState.lastMutation.content
}
},
sources: [toSource(target)]
});
}
const listingIntent = isListingIntent(userText);
const candidates = await collectCandidates(fetch, chat, userText, listingIntent ? 24 : 8);
if (isExplicitUpdateRequest(userText) && candidates[0] && (candidates[0].raw_content || candidates[0].extracted_text)) {
const target = candidates[0];
try {
const updatedContent = await rewriteExistingItemContent(chat, target, userText);
const updated = await updateBrainItem(fetch, target.id, { raw_content: updatedContent });
if (!updated.ok) {
return json({ reply: `I couldn't update "${target.title || 'Untitled'}" yet.`, state: currentState, sources: [] }, { status: 500 });
}
return json({
reply: `Updated "${target.title || 'Untitled'}".`,
state: {
lastMutation: {
type: 'update',
itemId: target.id,
itemTitle: target.title || 'Untitled',
content: updatedContent,
previousRawContent: target.raw_content || target.extracted_text || ''
}
},
sources: [toSource(target)]
});
} catch (error) {
return json(
{
reply: `I couldn't update "${target.title || 'Untitled'}" yet.`,
state: currentState,
sources: [toSource(target)],
error: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}
if (wantsDelete(userText) && !currentState.pendingDelete) {
const target = candidates[0];
if (!target) {
return json({ reply: "I couldn't find a note to delete.", state: currentState, sources: [] });
}
return json({
reply: `Delete "${target.title || 'Untitled'}"?`,
state: {
...currentState,
pendingDelete: {
itemId: target.id,
itemTitle: target.title || 'Untitled'
}
},
sources: [toSource(target)]
});
}
let decision;
try {
decision = await decideAction(chat, candidates, currentState, listingIntent);
} catch (error) {
return json(
{
reply: 'The Brain assistant did not respond cleanly.',
state: currentState,
sources: [],
error: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
if (decision.action === 'answer') {
return json({
reply: decision.answer || decision.reply || 'I found a few relevant notes.',
state: currentState,
sources: sourceLinksFromIds(candidates, decision.source_item_ids)
});
}
if (decision.action === 'list_items') {
return json({
reply: decision.answer || decision.reply || 'Here are the matching items I found.',
state: currentState,
sources: sourceLinksFromIds(candidates, decision.source_item_ids)
});
}
if (decision.action === 'delete_target' && decision.target_item_id) {
const target = candidates.find((item) => item.id === decision.target_item_id) || (await getItem(fetch, decision.target_item_id));
if (!target) {
return json({ reply: "I couldn't find that note to delete.", state: currentState, sources: [] });
}
return json({
reply: `Delete "${target.title || 'Untitled'}"?`,
state: {
...currentState,
pendingDelete: {
itemId: target.id,
itemTitle: target.title || 'Untitled'
}
},
sources: [toSource(target)]
});
}
if (decision.action === 'update_existing' && decision.target_item_id && decision.match_confidence === 'high') {
const target = candidates.find((item) => item.id === decision.target_item_id) || (await getItem(fetch, decision.target_item_id));
if (target && (target.raw_content || target.extracted_text)) {
try {
const updatedContent = await rewriteExistingItemContent(chat, target, userText);
const updated = await updateBrainItem(fetch, target.id, { raw_content: updatedContent });
if (!updated.ok) {
return json({ reply: `I couldn't update "${target.title || 'Untitled'}" yet.`, state: currentState, sources: [] }, { status: 500 });
}
return json({
reply: `Updated "${target.title || 'Untitled'}".`,
state: {
lastMutation: {
type: 'update',
itemId: target.id,
itemTitle: target.title || 'Untitled',
content: updatedContent,
previousRawContent: target.raw_content || target.extracted_text || ''
}
},
sources: [toSource(target)]
});
} catch (error) {
return json(
{
reply: `I couldn't update "${target.title || 'Untitled'}" yet.`,
state: currentState,
sources: [toSource(target)],
error: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}
}
const content = decision.formatted_content?.trim() || userText;
if (decision.action === 'append_existing' && decision.target_item_id && decision.match_confidence === 'high') {
const target = candidates.find((item) => item.id === decision.target_item_id) || (await getItem(fetch, decision.target_item_id));
if (!target) {
// fall through to create below
} else {
const appended = await appendToItem(fetch, target.id, content);
if (!appended.ok) {
return json({ reply: `I couldn't add that to "${target.title || 'Untitled'}" yet.`, state: currentState, sources: [] }, { status: 500 });
}
return json({
reply: `Added to "${target.title || 'Untitled'}".`,
state: {
lastMutation: {
type: 'append',
itemId: target.id,
itemTitle: target.title || 'Untitled',
additionId: appended.body?.id,
content
}
},
sources: [toSource(target)]
});
}
}
const created = await createBrainNote(fetch, content, decision.create_title);
if (!created.ok) {
return json({ reply: 'I could not create the note yet.', state: currentState, sources: [] }, { status: 500 });
}
const createdItem = created.body as BrainItem;
return json({
reply: `Created "${createdItem.title || decision.create_title || 'New note'}".`,
state: {
lastMutation: {
type: 'create',
itemId: createdItem.id,
createdItemId: createdItem.id,
itemTitle: createdItem.title || decision.create_title || 'New note',
content
}
},
sources: [toSource(createdItem)]
});
};