fc0f91d1e6
Projekte sind benannte Thema-Bündel die voice-gesteuert via Brain-Tools
geöffnet/verlassen werden. Default-Mode bleibt der Hauptthread — Projekte
sind eine optionale Bühne. Anchored-not-replaced: App-Open landet immer
im Hauptchat, Projekte sind nur sichtbar wenn aktiv betreten.
Brain:
- projects.py: CRUD + Fuzzy-Find + Active-State-Pointer
(/shared/config/projects.json + active_project.txt).
- conversation.py: Turn.project_id-Feld + window(project_id) Filter.
- agent.py: 6 Meta-Tools — project_create / _enter / _exit / _list /
_summary / _end. chat() liest aktive Projekt-ID, taggt User+Assistant-
Turns damit, filtert das LLM-Window auf Projekt-Kontext und ergaenzt
den System-Prompt um den aktiven Projekt-Hinweis. touch_project pflegt
last_activity_at + turn_count.
- main.py: REST-Endpoints /projects/{status,list,create,switch,
{id}/end,{id}/archive, PATCH /{id}}.
Bridge + RVS:
- aria_bridge.py: project_changed Event-Propagation Brain → RVS-Broadcast
damit App + Diagnostic ihre Banner refreshen.
- rvs/server.js: project_changed in ALLOWED_TYPES.
App:
- brainApi.ts: Project-Type + 6 API-Methoden.
- ProjectsBrowser.tsx (neue Komponente, ~340 Zeilen): Status-Header,
Hauptchat als Erster-Eintrag, Projekt-Liste mit Aktiv-Marker, Long-Press
zum Editieren, Modals fuer Neu/Edit/End/Archiv.
- ChatScreen.tsx: Banner unterhalb des Status-Bars zeigt aktives Projekt
oder „Hauptchat" — Tap öffnet ProjectsBrowser als Modal. Aktive Projekt-
Info wird bei Mount + bei project_changed-Events refreshed.
- SettingsScreen.tsx: Neue Section 📁 „Projekte" zeigt ProjectsBrowser inline.
Diagnostic:
- Neue Sektion im Brain-Tab mit Liste, Aktiv-Marker, Beenden/Archivieren
pro Zeile, Modal fuer Neu. Lädt automatisch bei Brain-Tab + bei
project_changed-Event-Broadcast.
Was bewusst NICHT drin ist (Folgeschritte):
- Per-Message Filter im Chat-Verlauf (zeigt aktuell alle Bubbles, Banner
zeigt Kontext) — App müsste Chat-History per project_id filtern.
- Files-by-Project Tagging.
- Inline-Collapse-Bloecke im Chat-Verlauf.
- Sub-Projekte (Stefan-Entscheidung: weglassen, „Mama-tauglich").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
596 lines
20 KiB
TypeScript
596 lines
20 KiB
TypeScript
/**
|
|
* Brain-API-Client fuer die App.
|
|
*
|
|
* Die App hat keinen direkten HTTP-Zugriff aufs Brain (nur via RVS). Wir
|
|
* tunneln alle Memory-Operationen ueber den generischen brain_request /
|
|
* brain_response RVS-Channel den die Bridge implementiert.
|
|
*
|
|
* Pattern: pro Call eine eindeutige requestId, Listener wartet auf passende
|
|
* brain_response, Promise loest auf / wird abgelehnt bei status>=400.
|
|
*/
|
|
|
|
import rvs from './rvs';
|
|
|
|
type AnyJson = any;
|
|
|
|
interface PendingRequest {
|
|
resolve: (data: AnyJson) => void;
|
|
reject: (err: Error) => void;
|
|
timer: ReturnType<typeof setTimeout>;
|
|
expectBinary?: boolean;
|
|
}
|
|
|
|
const pending = new Map<string, PendingRequest>();
|
|
let installed = false;
|
|
|
|
function _ensureListener() {
|
|
if (installed) return;
|
|
installed = true;
|
|
rvs.onMessage((msg: any) => {
|
|
if (!msg || msg.type !== 'brain_response') return;
|
|
const p = msg.payload || {};
|
|
const reqId: string = p.requestId || '';
|
|
const handler = pending.get(reqId);
|
|
if (!handler) return;
|
|
pending.delete(reqId);
|
|
clearTimeout(handler.timer);
|
|
const status: number = Number(p.status || 0);
|
|
if (status >= 200 && status < 300) {
|
|
if (handler.expectBinary) {
|
|
handler.resolve({ base64: p.base64 || '', contentType: p.contentType || '' });
|
|
} else {
|
|
handler.resolve(p.json !== undefined ? p.json : (p.text !== undefined ? p.text : null));
|
|
}
|
|
} else {
|
|
const detail = (p.json && p.json.detail) || p.text || `HTTP ${status}`;
|
|
handler.reject(new Error(`Brain ${status}: ${detail}`));
|
|
}
|
|
});
|
|
}
|
|
|
|
let _nextId = 0;
|
|
function _newRequestId(): string {
|
|
_nextId += 1;
|
|
return `brain_${Date.now().toString(36)}_${_nextId}`;
|
|
}
|
|
|
|
/** Mini-Query-String-Builder ohne URLSearchParams (Hermes-Polyfill kennt
|
|
* kein URLSearchParams.set, crasht). Akzeptiert object mit string/number/
|
|
* bool-Values; undefined/null/leere Strings werden ausgelassen. */
|
|
function _qs(params: Record<string, unknown>): string {
|
|
const parts: string[] = [];
|
|
for (const [k, v] of Object.entries(params)) {
|
|
if (v === undefined || v === null || v === '') continue;
|
|
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
}
|
|
return parts.length ? `?${parts.join('&')}` : '';
|
|
}
|
|
|
|
interface SendOpts {
|
|
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
body?: AnyJson;
|
|
bodyBase64?: string;
|
|
contentType?: string;
|
|
expectBinary?: boolean;
|
|
timeoutMs?: number;
|
|
}
|
|
|
|
function _send(path: string, opts: SendOpts = {}): Promise<AnyJson> {
|
|
_ensureListener();
|
|
// Fast-Fail wenn RVS nicht verbunden — sonst tickt der Timeout 30s und
|
|
// der TriggerBrowser / Dateimanager zeigt ne ewig drehende Spinner.
|
|
// Stefan-Bug 06/2026: "Connection refused, App haengt 30 Sekunden".
|
|
const rvsState = rvs.getState();
|
|
if (rvsState !== 'connected') {
|
|
return Promise.reject(new Error(
|
|
`Keine Verbindung zum Brain (RVS: ${rvsState}). Warte auf Reconnect...`,
|
|
));
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = _newRequestId();
|
|
const timer = setTimeout(() => {
|
|
if (pending.delete(requestId)) {
|
|
reject(new Error(`Brain-Timeout fuer ${path}`));
|
|
}
|
|
}, opts.timeoutMs || 30000);
|
|
pending.set(requestId, { resolve, reject, timer, expectBinary: opts.expectBinary });
|
|
rvs.send('brain_request' as any, {
|
|
requestId,
|
|
method: opts.method || 'GET',
|
|
path,
|
|
...(opts.body !== undefined ? { body: opts.body } : {}),
|
|
...(opts.bodyBase64 ? { bodyBase64: opts.bodyBase64 } : {}),
|
|
...(opts.contentType ? { contentType: opts.contentType } : {}),
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Typen ────────────────────────────────────────────────────────────
|
|
|
|
export interface MemoryAttachment {
|
|
name: string;
|
|
mime: string;
|
|
size: number;
|
|
path: string;
|
|
}
|
|
|
|
export interface Memory {
|
|
id: string;
|
|
type: string;
|
|
title: string;
|
|
content: string;
|
|
pinned: boolean;
|
|
category: string;
|
|
source: string;
|
|
tags: string[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
conversation_id?: string | null;
|
|
score?: number | null;
|
|
attachments?: MemoryAttachment[];
|
|
}
|
|
|
|
/** OAuth-Service-Status wie aus Brain `/oauth/services` zurueckkommt. */
|
|
export interface OAuthServiceStatus {
|
|
service: string;
|
|
configured: boolean;
|
|
authenticated: boolean;
|
|
expiresAt?: number | null;
|
|
expiresInSec?: number | null;
|
|
hasRefresh: boolean;
|
|
scope?: string;
|
|
isDefault: boolean;
|
|
}
|
|
|
|
/** OAuth-App-Config (client_id/scopes/URLs) — client_secret kommt NIE rausgegeben. */
|
|
export interface OAuthAppConfig {
|
|
client_id: string;
|
|
has_client_secret: boolean;
|
|
scopes?: string[] | null;
|
|
auth_url?: string | null;
|
|
token_url?: string | null;
|
|
}
|
|
|
|
/** Projekt — Stefans Threading-Konzept im Hauptchat. */
|
|
export interface Project {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
status: 'active' | 'ended' | 'archived';
|
|
created_at: number;
|
|
updated_at: number;
|
|
last_activity_at: number;
|
|
turn_count: number;
|
|
}
|
|
|
|
export interface ProjectStatus {
|
|
active_id: string;
|
|
active: Project | null;
|
|
projects: Project[];
|
|
}
|
|
|
|
/** Skill-Manifest wie aus Brain `/skills/list` zurueckkommt. */
|
|
export interface Skill {
|
|
name: string;
|
|
description: string;
|
|
execution: string; // local-venv | local-bin | bash
|
|
entry: string; // run.py | run.sh
|
|
args?: any[]; // [{name, type, required, description}]
|
|
requires?: { pip?: string[]; binaries?: string[] };
|
|
active: boolean;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
last_used?: string | null;
|
|
use_count?: number;
|
|
version?: string;
|
|
author?: string; // "aria" | "stefan"
|
|
setup_error?: string;
|
|
// P3: konfigurierbare Werte (API-Keys, IDs etc.) — Stefan setzt sie hier,
|
|
// Skill bekommt sie als CFG_<NAME> ENV. Werte selbst kommen via /config.
|
|
config_schema?: SkillConfigField[];
|
|
// P4: Versions-Historie. Detail-Liste kommt via /versions.
|
|
version_history?: { version_id: string; archived_at?: string; summary?: string }[];
|
|
}
|
|
|
|
export interface SkillConfigField {
|
|
name: string;
|
|
type: 'string' | 'number' | 'boolean' | 'password';
|
|
label?: string;
|
|
secret?: boolean;
|
|
description?: string;
|
|
default?: any;
|
|
}
|
|
|
|
export interface SkillVersion {
|
|
version_id: string;
|
|
archived_at?: string;
|
|
summary?: string;
|
|
}
|
|
|
|
/** Trigger-Manifest wie aus Brain `/triggers/list` zurueckkommt. */
|
|
export interface Trigger {
|
|
name: string;
|
|
type: 'timer' | 'watcher' | string;
|
|
active: boolean;
|
|
author?: string;
|
|
message: string;
|
|
fires_at?: string; // ISO, nur timer
|
|
condition?: string; // nur watcher
|
|
check_interval_sec?: number; // nur watcher
|
|
throttle_sec?: number; // nur watcher
|
|
fire_count?: number;
|
|
last_fired_at?: string | null;
|
|
last_checked_at?: string | null;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
// ── Memory CRUD ──────────────────────────────────────────────────────
|
|
|
|
export const brainApi = {
|
|
/** Einzelne Memory holen (mit allen Feldern inkl. Anhaenge) */
|
|
getMemory(id: string): Promise<Memory> {
|
|
return _send(`/memory/get/${encodeURIComponent(id)}`);
|
|
},
|
|
|
|
/** Liste aller Memories, optional nach Type gefiltert. */
|
|
listMemories(opts: { type?: string; limit?: number } = {}): Promise<Memory[]> {
|
|
const qs = _qs({ type: opts.type, limit: opts.limit || 500 });
|
|
return _send(`/memory/list${qs}`);
|
|
},
|
|
|
|
/** Volltext-Substring-Suche. */
|
|
searchText(q: string, opts: { type?: string; includePinned?: boolean; k?: number } = {}): Promise<Memory[]> {
|
|
const qs = _qs({
|
|
q,
|
|
type: opts.type,
|
|
include_pinned: opts.includePinned !== false,
|
|
k: opts.k || 50,
|
|
});
|
|
return _send(`/memory/search-text${qs}`);
|
|
},
|
|
|
|
/** Semantische Suche (Embedder). */
|
|
searchSemantic(q: string, opts: { type?: string; includePinned?: boolean; k?: number; threshold?: number } = {}): Promise<Memory[]> {
|
|
const qs = _qs({
|
|
q,
|
|
type: opts.type,
|
|
include_pinned: opts.includePinned !== false,
|
|
k: opts.k || 10,
|
|
score_threshold: opts.threshold ?? 0.30,
|
|
});
|
|
return _send(`/memory/search${qs}`);
|
|
},
|
|
|
|
/** Memory anlegen. */
|
|
saveMemory(body: {
|
|
type: string;
|
|
title: string;
|
|
content: string;
|
|
pinned?: boolean;
|
|
category?: string;
|
|
tags?: string[];
|
|
}): Promise<Memory> {
|
|
return _send('/memory/save', {
|
|
method: 'POST',
|
|
body: { source: 'app', ...body },
|
|
});
|
|
},
|
|
|
|
/** Memory aktualisieren (Patch — nur uebergebene Felder werden geaendert). */
|
|
updateMemory(id: string, body: Partial<Pick<Memory, 'title' | 'content' | 'pinned' | 'category' | 'tags'>>): Promise<Memory> {
|
|
return _send(`/memory/update/${encodeURIComponent(id)}`, {
|
|
method: 'PATCH',
|
|
body,
|
|
});
|
|
},
|
|
|
|
/** Memory loeschen. */
|
|
deleteMemory(id: string): Promise<{ deleted: string }> {
|
|
return _send(`/memory/delete/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
// ── Anhaenge ────────────────────────────────────────────────────────
|
|
|
|
/** Datei als Anhang an die Memory haengen (Base64-Upload). */
|
|
uploadAttachment(memoryId: string, name: string, base64: string): Promise<Memory> {
|
|
return _send(`/memory/${encodeURIComponent(memoryId)}/attachments`, {
|
|
method: 'POST',
|
|
body: { name, data_base64: base64 },
|
|
timeoutMs: 120000,
|
|
});
|
|
},
|
|
|
|
/** Anhang loeschen. */
|
|
deleteAttachment(memoryId: string, filename: string): Promise<Memory> {
|
|
return _send(
|
|
`/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`,
|
|
{ method: 'DELETE' },
|
|
);
|
|
},
|
|
|
|
/** Anhang-Bytes holen (fuer Vorschau / Download). Liefert Base64. */
|
|
getAttachmentBytes(memoryId: string, filename: string): Promise<{ base64: string; contentType: string }> {
|
|
return _send(
|
|
`/memory/${encodeURIComponent(memoryId)}/attachments/${encodeURIComponent(filename)}`,
|
|
{ expectBinary: true, timeoutMs: 60000 },
|
|
);
|
|
},
|
|
|
|
// ── Triggers ────────────────────────────────────────────────────────
|
|
|
|
/** Liste aller Trigger (aktive + inaktive).
|
|
* Brain returnt {triggers: [...]} — wir unwrappen damit der Caller einfach
|
|
* t.sort/filter/map nutzen kann. Ohne das Unwrap warf t.sort() eine
|
|
* TypeError-Exception und der TriggerBrowser blieb leer. */
|
|
listTriggers(): Promise<Trigger[]> {
|
|
return _send('/triggers/list').then((r: any) => Array.isArray(r) ? r : (r?.triggers || []));
|
|
},
|
|
|
|
/** Einzelnen Trigger holen (inkl. fire_count, last_fired_at, ...). */
|
|
getTrigger(name: string): Promise<Trigger> {
|
|
return _send(`/triggers/${encodeURIComponent(name)}`);
|
|
},
|
|
|
|
/** Verfuegbare Condition-Variablen + Funktionen (fuer Watcher-Editor). */
|
|
getTriggerConditions(): Promise<{ variables: any[]; functions: any[] }> {
|
|
return _send('/triggers/conditions');
|
|
},
|
|
|
|
/** Trigger-Logs (last N Feuerungen). */
|
|
getTriggerLogs(name: string, limit: number = 50): Promise<any[]> {
|
|
return _send(`/triggers/${encodeURIComponent(name)}/logs?limit=${limit}`);
|
|
},
|
|
|
|
/** Timer anlegen. fires_at = ISO timestamp (UTC). */
|
|
createTimer(body: { name: string; fires_at: string; message: string; author?: string }): Promise<Trigger> {
|
|
return _send('/triggers/timer', {
|
|
method: 'POST',
|
|
body: { author: 'app', ...body },
|
|
});
|
|
},
|
|
|
|
/** Watcher anlegen. */
|
|
createWatcher(body: {
|
|
name: string;
|
|
condition: string;
|
|
message: string;
|
|
check_interval_sec?: number;
|
|
throttle_sec?: number;
|
|
author?: string;
|
|
}): Promise<Trigger> {
|
|
return _send('/triggers/watcher', {
|
|
method: 'POST',
|
|
body: { author: 'app', ...body },
|
|
});
|
|
},
|
|
|
|
/** Trigger patchen (active/message/condition/throttle/interval/fires_at). */
|
|
updateTrigger(name: string, body: Partial<{
|
|
active: boolean;
|
|
message: string;
|
|
condition: string;
|
|
throttle_sec: number;
|
|
check_interval_sec: number;
|
|
fires_at: string;
|
|
}>): Promise<Trigger> {
|
|
return _send(`/triggers/${encodeURIComponent(name)}`, {
|
|
method: 'PATCH',
|
|
body,
|
|
});
|
|
},
|
|
|
|
/** Trigger loeschen. */
|
|
deleteTrigger(name: string): Promise<{ deleted: string }> {
|
|
return _send(`/triggers/${encodeURIComponent(name)}`, {
|
|
method: 'DELETE',
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
// ── Skills ────────────────────────────────────────────────────────
|
|
|
|
/** Liste aller Skills (aktive + inaktive). Brain returnt {skills: [...]}. */
|
|
listSkills(): Promise<Skill[]> {
|
|
return _send('/skills/list').then((r: any) => Array.isArray(r) ? r : (r?.skills || []));
|
|
},
|
|
|
|
/** Einzelnen Skill holen (inkl. setup_error, last_used, use_count). */
|
|
getSkill(name: string): Promise<Skill> {
|
|
return _send(`/skills/${encodeURIComponent(name)}`);
|
|
},
|
|
|
|
/** Skill ausfuehren (mit args als ENV ARG_XXX). Skill-Run kann lange dauern,
|
|
* 5 min Default-Timeout. */
|
|
runSkill(name: string, args: Record<string, any> = {}): Promise<{
|
|
ok: boolean; exit_code: number; stdout: string; stderr: string;
|
|
duration_sec: number; log_path?: string;
|
|
}> {
|
|
return _send('/skills/run', {
|
|
method: 'POST',
|
|
body: { name, args, timeout_sec: 300 },
|
|
timeoutMs: 320000,
|
|
});
|
|
},
|
|
|
|
/** Skill-Manifest aendern (description, active, args...). Code-Aenderungen
|
|
* gehen ueber ARIAs eigene skill_update-Tool — die App-UI sollte sie
|
|
* NICHT direkt anbieten (zu fehleranfaellig). */
|
|
updateSkill(name: string, body: Partial<{
|
|
description: string;
|
|
active: boolean;
|
|
args: any[];
|
|
version: string;
|
|
}>): Promise<Skill> {
|
|
return _send(`/skills/${encodeURIComponent(name)}`, {
|
|
method: 'PATCH',
|
|
body,
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
/** Skill loeschen (samt venv + logs). */
|
|
deleteSkill(name: string): Promise<{ deleted: string }> {
|
|
return _send(`/skills/${encodeURIComponent(name)}`, {
|
|
method: 'DELETE',
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
/** Letzte Run-Logs eines Skills. */
|
|
getSkillLogs(name: string, limit: number = 20): Promise<any[]> {
|
|
return _send(`/skills/${encodeURIComponent(name)}/logs?limit=${limit}`)
|
|
.then((r: any) => Array.isArray(r) ? r : (r?.logs || []));
|
|
},
|
|
|
|
/** P3: Config-Schema + aktuelle Werte (secret-Felder gemaskt mit '***SET***'). */
|
|
getSkillConfig(name: string): Promise<{ schema: SkillConfigField[]; values: Record<string, any> }> {
|
|
return _send(`/skills/${encodeURIComponent(name)}/config`)
|
|
.then((r: any) => ({ schema: r?.schema || [], values: r?.values || {} }));
|
|
},
|
|
|
|
/** P3: Config-Werte komplett ueberschreiben. Werte greifen ab dem naechsten Run. */
|
|
setSkillConfig(name: string, values: Record<string, any>): Promise<{ ok: boolean; values: Record<string, any> }> {
|
|
return _send(`/skills/${encodeURIComponent(name)}/config`, {
|
|
method: 'POST',
|
|
body: { values },
|
|
timeoutMs: 10000,
|
|
});
|
|
},
|
|
|
|
/** P4: Liste archivierter Versionen, neueste zuerst. */
|
|
listSkillVersions(name: string): Promise<SkillVersion[]> {
|
|
return _send(`/skills/${encodeURIComponent(name)}/versions`)
|
|
.then((r: any) => r?.versions || []);
|
|
},
|
|
|
|
/** P4: Rollback auf eine fruehere Version. Aktueller Stand wird automatisch gesichert. */
|
|
rollbackSkill(name: string, versionId: string): Promise<{ ok: boolean; rolled_back_to: string; safety_snapshot: string }> {
|
|
return _send(`/skills/${encodeURIComponent(name)}/rollback`, {
|
|
method: 'POST',
|
|
body: { version_id: versionId },
|
|
timeoutMs: 60000, // venv-Rebuild kann dauern
|
|
});
|
|
},
|
|
|
|
/** P4: Einzelne Version dauerhaft loeschen. */
|
|
deleteSkillVersion(name: string, versionId: string): Promise<{ ok: boolean; deleted: string }> {
|
|
return _send(`/skills/${encodeURIComponent(name)}/versions/${encodeURIComponent(versionId)}`, {
|
|
method: 'DELETE',
|
|
timeoutMs: 10000,
|
|
});
|
|
},
|
|
|
|
// ── OAuth ────────────────────────────────────────────────────────
|
|
|
|
/** Liste aller Services mit Auth-Status (configured/authenticated/expires). */
|
|
listOAuthServices(): Promise<{ services: OAuthServiceStatus[] }> {
|
|
return _send('/oauth/services');
|
|
},
|
|
|
|
/** Persistierte Provider-Configs (URLs/scopes/client_id, KEIN client_secret). */
|
|
getOAuthApps(): Promise<{ apps: Record<string, OAuthAppConfig>; defaults: string[] }> {
|
|
return _send('/oauth/apps');
|
|
},
|
|
|
|
/** Provider-Config setzen/aktualisieren. Leerer client_secret laesst
|
|
* den bestehenden Wert stehen. */
|
|
saveOAuthApp(body: {
|
|
service: string;
|
|
client_id?: string;
|
|
client_secret?: string;
|
|
scopes?: string[];
|
|
auth_url?: string;
|
|
token_url?: string;
|
|
}): Promise<{ ok: boolean; service: string }> {
|
|
return _send('/oauth/apps', {
|
|
method: 'POST',
|
|
body,
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
/** Service-Eintrag komplett entfernen (incl. Token). */
|
|
deleteOAuthApp(service: string): Promise<{ ok: boolean }> {
|
|
return _send(`/oauth/apps/${encodeURIComponent(service)}`, {
|
|
method: 'DELETE',
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
/** Authorize-URL bauen (Brain speichert state, gibt url + redirect_uri zurueck). */
|
|
authorizeOAuth(service: string, scopes?: string[]): Promise<{
|
|
url: string; state: string; redirect_uri: string; service: string;
|
|
}> {
|
|
return _send('/oauth/authorize', {
|
|
method: 'POST',
|
|
body: { service, scopes },
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
/** Token loeschen (lokal — kein Provider-Revoke). */
|
|
revokeOAuth(service: string): Promise<{ ok: boolean }> {
|
|
return _send(`/oauth/${encodeURIComponent(service)}/revoke`, {
|
|
method: 'POST',
|
|
timeoutMs: 15000,
|
|
});
|
|
},
|
|
|
|
// ── Projekte ───────────────────────────────────────────────────
|
|
|
|
/** Kompletter Status: aktives Projekt + Liste. */
|
|
getProjectStatus(): Promise<ProjectStatus> {
|
|
return _send('/projects/status');
|
|
},
|
|
|
|
/** Nur die Liste — fuer Sidebar/Drawer. */
|
|
listProjects(includeArchived: boolean = false): Promise<Project[]> {
|
|
return _send(`/projects/list${includeArchived ? '?include_archived=true' : ''}`)
|
|
.then((r: any) => r?.projects || []);
|
|
},
|
|
|
|
/** Neues Projekt anlegen — wird automatisch aktiviert. */
|
|
createProject(body: { name: string; description?: string }): Promise<Project> {
|
|
return _send('/projects/create', {
|
|
method: 'POST',
|
|
body: { description: '', ...body },
|
|
});
|
|
},
|
|
|
|
/** Aktives Projekt wechseln. Leerer projectId = Hauptthread. */
|
|
switchProject(projectId: string): Promise<ProjectStatus> {
|
|
return _send('/projects/switch', {
|
|
method: 'POST',
|
|
body: { project_id: projectId },
|
|
});
|
|
},
|
|
|
|
/** Projekt als beendet markieren (bleibt sichtbar, aktiv ist dann der Hauptthread). */
|
|
endProject(projectId: string): Promise<Project> {
|
|
return _send(`/projects/${encodeURIComponent(projectId)}/end`, {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
/** Projekt archivieren (verschwindet aus der Default-Liste). */
|
|
archiveProject(projectId: string): Promise<{ id: string; status: string }> {
|
|
return _send(`/projects/${encodeURIComponent(projectId)}/archive`, {
|
|
method: 'POST',
|
|
});
|
|
},
|
|
|
|
/** Projekt-Metadaten patchen (name / description). */
|
|
updateProject(projectId: string, patch: Partial<Pick<Project, 'name' | 'description'>>): Promise<Project> {
|
|
return _send(`/projects/${encodeURIComponent(projectId)}`, {
|
|
method: 'PATCH',
|
|
body: patch,
|
|
});
|
|
},
|
|
};
|
|
|
|
export default brainApi;
|