agent-office 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +41 -0
  5. package/dist/commands/manage.d.ts +5 -0
  6. package/dist/commands/manage.js +20 -0
  7. package/dist/commands/serve.d.ts +9 -0
  8. package/dist/commands/serve.js +54 -0
  9. package/dist/commands/worker.d.ts +1 -0
  10. package/dist/commands/worker.js +50 -0
  11. package/dist/db/index.d.ts +10 -0
  12. package/dist/db/index.js +9 -0
  13. package/dist/db/migrate.d.ts +2 -0
  14. package/dist/db/migrate.js +45 -0
  15. package/dist/lib/opencode.d.ts +7 -0
  16. package/dist/lib/opencode.js +4 -0
  17. package/dist/manage/app.d.ts +6 -0
  18. package/dist/manage/app.js +102 -0
  19. package/dist/manage/components/AgentCode.d.ts +8 -0
  20. package/dist/manage/components/AgentCode.js +73 -0
  21. package/dist/manage/components/CreateSession.d.ts +7 -0
  22. package/dist/manage/components/CreateSession.js +37 -0
  23. package/dist/manage/components/DeleteSession.d.ts +7 -0
  24. package/dist/manage/components/DeleteSession.js +55 -0
  25. package/dist/manage/components/InjectText.d.ts +8 -0
  26. package/dist/manage/components/InjectText.js +51 -0
  27. package/dist/manage/components/SessionList.d.ts +8 -0
  28. package/dist/manage/components/SessionList.js +52 -0
  29. package/dist/manage/components/TailMessages.d.ts +8 -0
  30. package/dist/manage/components/TailMessages.js +77 -0
  31. package/dist/manage/hooks/useApi.d.ts +33 -0
  32. package/dist/manage/hooks/useApi.js +82 -0
  33. package/dist/server/index.d.ts +3 -0
  34. package/dist/server/index.js +22 -0
  35. package/dist/server/routes.d.ts +5 -0
  36. package/dist/server/routes.js +228 -0
  37. package/package.json +50 -0
@@ -0,0 +1,7 @@
1
+ interface CreateSessionProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ }
6
+ export declare function CreateSession({ serverUrl, password, onBack }: CreateSessionProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { TextInput, Spinner } from "@inkjs/ui";
5
+ import { useApi, useAsyncState } from "../hooks/useApi.js";
6
+ export function CreateSession({ serverUrl, password, onBack }) {
7
+ const { createSession } = useApi(serverUrl, password);
8
+ const { loading, error, run } = useAsyncState();
9
+ const [created, setCreated] = useState(null);
10
+ const [submitted, setSubmitted] = useState(false);
11
+ // Auto-return to menu 1.5s after success or error
12
+ useEffect(() => {
13
+ if (created || error) {
14
+ const timer = setTimeout(onBack, 1500);
15
+ return () => clearTimeout(timer);
16
+ }
17
+ }, [created, error, onBack]);
18
+ const handleSubmit = async (name) => {
19
+ const trimmed = name.trim();
20
+ if (!trimmed)
21
+ return;
22
+ setSubmitted(true);
23
+ const result = await run(() => createSession(trimmed));
24
+ if (result)
25
+ setCreated(result);
26
+ };
27
+ if (loading) {
28
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Create Session" }), _jsx(Spinner, { label: "Creating session on OpenCode..." })] }));
29
+ }
30
+ if (created) {
31
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Create Session" }), _jsx(Text, { color: "green", children: "Session created!" }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Name:" }), _jsx(Text, { color: "cyan", children: created.name })] }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "OpenCode ID:" }), _jsx(Text, { dimColor: true, children: created.session_id })] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
32
+ }
33
+ if (error) {
34
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Create Session" }), _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
35
+ }
36
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Create Session" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Name: " }), !submitted && (_jsx(TextInput, { placeholder: "e.g. john", onSubmit: handleSubmit }))] })] }));
37
+ }
@@ -0,0 +1,7 @@
1
+ interface DeleteSessionProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ }
6
+ export declare function DeleteSession({ serverUrl, password, onBack }: DeleteSessionProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { Select, Spinner, ConfirmInput } from "@inkjs/ui";
5
+ import { useApi, useAsyncState } from "../hooks/useApi.js";
6
+ export function DeleteSession({ serverUrl, password, onBack }) {
7
+ const { listSessions, deleteSession } = useApi(serverUrl, password);
8
+ const { run } = useAsyncState();
9
+ const [sessions, setSessions] = useState([]);
10
+ const [stage, setStage] = useState("loading");
11
+ const [selected, setSelected] = useState(null);
12
+ const [error, setError] = useState(null);
13
+ useEffect(() => {
14
+ void run(listSessions).then((rows) => {
15
+ if (rows && rows.length > 0) {
16
+ setSessions(rows);
17
+ setStage("select");
18
+ }
19
+ else if (rows) {
20
+ setError("No sessions to delete.");
21
+ setStage("error");
22
+ }
23
+ });
24
+ }, []);
25
+ // Auto-return to menu 1.5s after done or error
26
+ useEffect(() => {
27
+ if (stage === "done" || stage === "error") {
28
+ const timer = setTimeout(onBack, 1500);
29
+ return () => clearTimeout(timer);
30
+ }
31
+ }, [stage, onBack]);
32
+ const handleSelect = (name) => {
33
+ setSelected(name);
34
+ setStage("confirm");
35
+ };
36
+ const handleConfirm = async (confirmed) => {
37
+ if (!confirmed || !selected) {
38
+ onBack();
39
+ return;
40
+ }
41
+ setStage("deleting");
42
+ try {
43
+ await deleteSession(selected);
44
+ setStage("done");
45
+ }
46
+ catch (err) {
47
+ setError(err instanceof Error ? err.message : String(err));
48
+ setStage("error");
49
+ }
50
+ };
51
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Delete Session" }), stage === "loading" && (_jsx(Spinner, { label: "Loading sessions..." })), stage === "select" && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Select a session to delete:" }), _jsx(Select, { options: sessions.map((s) => ({
52
+ label: `${s.name} ${s.session_id.slice(0, 8)}...`,
53
+ value: s.name,
54
+ })), onChange: handleSelect })] })), stage === "confirm" && selected && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["Delete ", _jsx(Text, { color: "yellow", bold: true, children: selected }), "?", " ", _jsx(Text, { dimColor: true, children: "This also deletes the OpenCode session." })] }), _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleConfirm(true), onCancel: () => void handleConfirm(false) })] })), stage === "deleting" && (_jsx(Spinner, { label: `Deleting "${selected}"...` })), stage === "done" && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "green", children: ["Session \"", selected, "\" deleted."] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] })), stage === "error" && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }))] }));
55
+ }
@@ -0,0 +1,8 @@
1
+ interface InjectTextProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ contentHeight: number;
6
+ }
7
+ export declare function InjectText({ serverUrl, password, onBack, contentHeight }: InjectTextProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { Select, Spinner, TextInput } from "@inkjs/ui";
5
+ import { useApi, useAsyncState } from "../hooks/useApi.js";
6
+ export function InjectText({ serverUrl, password, onBack, contentHeight }) {
7
+ const { listSessions, injectText } = useApi(serverUrl, password);
8
+ const { run: runList } = useAsyncState();
9
+ const [sessions, setSessions] = useState([]);
10
+ const [sessionsLoading, setSessionsLoading] = useState(true);
11
+ const [stage, setStage] = useState("select");
12
+ const [selectedName, setSelectedName] = useState(null);
13
+ const [error, setError] = useState(null);
14
+ const [submitted, setSubmitted] = useState(false);
15
+ useEffect(() => {
16
+ runList(listSessions).then((rows) => {
17
+ setSessions(rows ?? []);
18
+ setSessionsLoading(false);
19
+ });
20
+ }, []);
21
+ // Auto-return after done or error
22
+ useEffect(() => {
23
+ if (stage === "done" || stage === "error") {
24
+ const timer = setTimeout(onBack, 2000);
25
+ return () => clearTimeout(timer);
26
+ }
27
+ }, [stage, onBack]);
28
+ const handleSessionSelect = (name) => {
29
+ setSelectedName(name);
30
+ setStage("input");
31
+ };
32
+ const handleTextSubmit = async (text) => {
33
+ const trimmed = text.trim();
34
+ if (!trimmed || !selectedName)
35
+ return;
36
+ setSubmitted(true);
37
+ setStage("submitting");
38
+ try {
39
+ await injectText(selectedName, trimmed);
40
+ setStage("done");
41
+ }
42
+ catch (err) {
43
+ setError(err instanceof Error ? err.message : String(err));
44
+ setStage("error");
45
+ }
46
+ };
47
+ if (sessionsLoading) {
48
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
49
+ }
50
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Inject Text" }), stage === "select" && sessions.length === 0 && (_jsx(Box, { height: contentHeight - 2, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) })), stage === "select" && sessions.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Select a session to inject into:" }), _jsx(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: handleSessionSelect })] })), stage === "input" && (_jsxs(_Fragment, { children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Session: " }), _jsx(Text, { color: "cyan", children: selectedName })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Text: " }), !submitted && (_jsx(TextInput, { placeholder: "Type your message...", onSubmit: handleTextSubmit }))] })] })), stage === "submitting" && (_jsx(Spinner, { label: `Injecting into "${selectedName}"...` })), stage === "done" && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "green", children: ["Message injected into \"", selectedName, "\"."] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] })), stage === "error" && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }))] }));
51
+ }
@@ -0,0 +1,8 @@
1
+ interface SessionListProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ contentHeight: number;
6
+ }
7
+ export declare function SessionList({ serverUrl, password, contentHeight }: SessionListProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { Spinner } from "@inkjs/ui";
5
+ import { useApi, useAsyncState } from "../hooks/useApi.js";
6
+ const MASKED_CODE = "••••••••-••••-••••-••••-••••••••••••";
7
+ export function SessionList({ serverUrl, password, contentHeight }) {
8
+ const { listSessions } = useApi(serverUrl, password);
9
+ const { data: sessions, loading, error, run } = useAsyncState();
10
+ const [cursor, setCursor] = useState(0);
11
+ const [revealedRows, setRevealedRows] = useState(new Set());
12
+ useEffect(() => {
13
+ void run(listSessions);
14
+ }, []);
15
+ const rows = sessions ?? [];
16
+ useInput((input, key) => {
17
+ if (loading || rows.length === 0)
18
+ return;
19
+ if (key.upArrow) {
20
+ setCursor((c) => Math.max(0, c - 1));
21
+ }
22
+ if (key.downArrow) {
23
+ setCursor((c) => Math.min(rows.length - 1, c + 1));
24
+ }
25
+ if (input === "r") {
26
+ const id = rows[cursor]?.id;
27
+ if (id == null)
28
+ return;
29
+ setRevealedRows((prev) => {
30
+ const next = new Set(prev);
31
+ if (next.has(id)) {
32
+ next.delete(id);
33
+ }
34
+ else {
35
+ next.add(id);
36
+ }
37
+ return next;
38
+ });
39
+ }
40
+ });
41
+ if (loading) {
42
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
43
+ }
44
+ if (error) {
45
+ return (_jsxs(Box, { height: contentHeight, flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error" }), _jsx(Text, { children: error })] }));
46
+ }
47
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { bold: true, children: ["Sessions ", _jsxs(Text, { dimColor: true, children: ["(", rows.length, ")"] })] }), rows.length === 0 ? (_jsx(Box, { height: contentHeight - 2, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: " NAME".padEnd(18) }), _jsx(Text, { bold: true, color: "cyan", children: "OPENCODE SESSION ID".padEnd(36) }), _jsx(Text, { bold: true, color: "cyan", children: "AGENT CODE" })] }), rows.map((s, i) => {
48
+ const selected = i === cursor;
49
+ const revealed = revealedRows.has(s.id);
50
+ return (_jsxs(Box, { gap: 2, children: [_jsxs(Box, { width: 18, children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: selected ? "▶ " : " " }), _jsx(Text, { color: selected ? "cyan" : "green", bold: selected, children: s.name })] }), _jsx(Text, { dimColor: !selected, children: s.session_id.padEnd(36) }), revealed ? (_jsx(Text, { color: "yellow", children: s.agent_code })) : (_jsx(Text, { dimColor: true, children: MASKED_CODE }))] }, s.id));
51
+ })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 r reveal/hide code \u00B7 Esc back" }) })] }));
52
+ }
@@ -0,0 +1,8 @@
1
+ interface TailMessagesProps {
2
+ serverUrl: string;
3
+ password: string;
4
+ onBack: () => void;
5
+ contentHeight: number;
6
+ }
7
+ export declare function TailMessages({ serverUrl, password, onBack, contentHeight }: TailMessagesProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { Select, Spinner } from "@inkjs/ui";
5
+ import { useApi, useAsyncState } from "../hooks/useApi.js";
6
+ export function TailMessages({ serverUrl, password, onBack, contentHeight }) {
7
+ const { listSessions, getMessages } = useApi(serverUrl, password);
8
+ const { run: runList } = useAsyncState();
9
+ const [sessions, setSessions] = useState([]);
10
+ const [sessionsLoading, setSessionsLoading] = useState(true);
11
+ const [stage, setStage] = useState("select");
12
+ const [selectedName, setSelectedName] = useState(null);
13
+ const [messages, setMessages] = useState([]);
14
+ const [error, setError] = useState(null);
15
+ const [scrollOffset, setScrollOffset] = useState(0);
16
+ useEffect(() => {
17
+ runList(listSessions).then((rows) => {
18
+ setSessions(rows ?? []);
19
+ setSessionsLoading(false);
20
+ });
21
+ }, []);
22
+ // Scroll through messages with arrow keys when viewing
23
+ useInput((input, key) => {
24
+ if (stage !== "viewing")
25
+ return;
26
+ if (key.upArrow)
27
+ setScrollOffset((o) => Math.max(0, o - 1));
28
+ if (key.downArrow)
29
+ setScrollOffset((o) => o + 1);
30
+ });
31
+ const handleSelect = async (name) => {
32
+ setSelectedName(name);
33
+ setStage("loading");
34
+ try {
35
+ const msgs = await getMessages(name, 50);
36
+ setMessages(msgs);
37
+ setScrollOffset(0);
38
+ setStage("viewing");
39
+ }
40
+ catch (err) {
41
+ setError(err instanceof Error ? err.message : String(err));
42
+ setStage("error");
43
+ }
44
+ };
45
+ if (sessionsLoading) {
46
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
47
+ }
48
+ if (stage === "select") {
49
+ if (sessions.length === 0) {
50
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) }));
51
+ }
52
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Tail Messages" }), _jsx(Text, { dimColor: true, children: "Select a session:" }), _jsx(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: handleSelect })] }));
53
+ }
54
+ if (stage === "loading") {
55
+ return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: `Fetching messages for "${selectedName}"...` }) }));
56
+ }
57
+ if (stage === "error") {
58
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Tail Messages" }), _jsxs(Text, { color: "red", children: ["Error: ", error] })] }));
59
+ }
60
+ // viewing — render messages with scroll
61
+ const messageLines = [];
62
+ for (const msg of messages) {
63
+ for (const part of msg.parts) {
64
+ // Split multiline text into individual lines for rendering
65
+ const lines = part.text.split("\n");
66
+ for (let i = 0; i < lines.length; i++) {
67
+ messageLines.push({ role: msg.role, text: i === 0 ? lines[i] : ` ${lines[i]}` });
68
+ }
69
+ messageLines.push({ role: msg.role, text: "" }); // blank line between parts
70
+ }
71
+ }
72
+ const viewHeight = contentHeight - 3; // header + separator
73
+ const maxOffset = Math.max(0, messageLines.length - viewHeight);
74
+ const clampedOffset = Math.min(scrollOffset, maxOffset);
75
+ const visible = messageLines.slice(clampedOffset, clampedOffset + viewHeight);
76
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Messages" }), _jsx(Text, { color: "cyan", children: selectedName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " messages)"] })] }), _jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { dimColor: true, children: "No messages in this session yet." })) : (visible.map((line, i) => (_jsx(Box, { children: line.role === "user" ? (_jsx(Text, { color: "green", children: line.text })) : (_jsx(Text, { children: line.text })) }, i)))) }), messageLines.length > viewHeight && (_jsxs(Text, { dimColor: true, children: ["[", clampedOffset + 1, "-", Math.min(clampedOffset + viewHeight, messageLines.length), "/", messageLines.length, " lines] \u2191\u2193 scroll"] }))] }));
77
+ }
@@ -0,0 +1,33 @@
1
+ export interface Session {
2
+ id: number;
3
+ name: string;
4
+ session_id: string;
5
+ agent_code: string;
6
+ created_at: string;
7
+ }
8
+ export interface MessagePart {
9
+ type: "text";
10
+ text: string;
11
+ }
12
+ export interface SessionMessage {
13
+ role: "user" | "assistant";
14
+ parts: MessagePart[];
15
+ }
16
+ export declare function useApi(serverUrl: string, password: string): {
17
+ listSessions: () => Promise<Session[]>;
18
+ createSession: (name: string) => Promise<Session>;
19
+ deleteSession: (name: string) => Promise<void>;
20
+ checkHealth: () => Promise<boolean>;
21
+ getMessages: (name: string, limit?: number) => Promise<SessionMessage[]>;
22
+ injectText: (name: string, text: string) => Promise<{
23
+ ok: boolean;
24
+ messageID: string;
25
+ }>;
26
+ regenerateCode: (name: string) => Promise<Session>;
27
+ };
28
+ export declare function useAsyncState<T>(): {
29
+ run: (fn: () => Promise<T>) => Promise<T | null>;
30
+ data: T | null;
31
+ loading: boolean;
32
+ error: string | null;
33
+ };
@@ -0,0 +1,82 @@
1
+ import { useState, useCallback } from "react";
2
+ function makeHeaders(password) {
3
+ return {
4
+ "Content-Type": "application/json",
5
+ Authorization: `Bearer ${password}`,
6
+ };
7
+ }
8
+ async function apiFetch(url, password, init) {
9
+ const res = await fetch(url, {
10
+ ...init,
11
+ headers: {
12
+ ...makeHeaders(password),
13
+ ...init?.headers,
14
+ },
15
+ });
16
+ if (res.status === 401) {
17
+ throw new Error("Unauthorized: check your --password");
18
+ }
19
+ const body = await res.json();
20
+ if (!res.ok) {
21
+ const msg = body.error ?? `HTTP ${res.status}`;
22
+ throw new Error(msg);
23
+ }
24
+ return body;
25
+ }
26
+ export function useApi(serverUrl, password) {
27
+ const base = serverUrl.replace(/\/$/, "");
28
+ const listSessions = useCallback(async () => {
29
+ return apiFetch(`${base}/sessions`, password);
30
+ }, [base, password]);
31
+ const createSession = useCallback(async (name) => {
32
+ return apiFetch(`${base}/sessions`, password, {
33
+ method: "POST",
34
+ body: JSON.stringify({ name }),
35
+ });
36
+ }, [base, password]);
37
+ const deleteSession = useCallback(async (name) => {
38
+ await apiFetch(`${base}/sessions/${encodeURIComponent(name)}`, password, {
39
+ method: "DELETE",
40
+ });
41
+ }, [base, password]);
42
+ const checkHealth = useCallback(async () => {
43
+ try {
44
+ await apiFetch(`${base}/health`, password);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }, [base, password]);
51
+ const getMessages = useCallback(async (name, limit = 20) => {
52
+ return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/messages?limit=${limit}`, password);
53
+ }, [base, password]);
54
+ const injectText = useCallback(async (name, text) => {
55
+ return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/inject`, password, { method: "POST", body: JSON.stringify({ text }) });
56
+ }, [base, password]);
57
+ const regenerateCode = useCallback(async (name) => {
58
+ return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/regenerate-code`, password, { method: "POST" });
59
+ }, [base, password]);
60
+ return { listSessions, createSession, deleteSession, checkHealth, getMessages, injectText, regenerateCode };
61
+ }
62
+ export function useAsyncState() {
63
+ const [state, setState] = useState({
64
+ data: null,
65
+ loading: false,
66
+ error: null,
67
+ });
68
+ const run = useCallback(async (fn) => {
69
+ setState({ data: null, loading: true, error: null });
70
+ try {
71
+ const data = await fn();
72
+ setState({ data, loading: false, error: null });
73
+ return data;
74
+ }
75
+ catch (err) {
76
+ const error = err instanceof Error ? err.message : String(err);
77
+ setState({ data: null, loading: false, error });
78
+ return null;
79
+ }
80
+ }, []);
81
+ return { ...state, run };
82
+ }
@@ -0,0 +1,3 @@
1
+ import type { Sql } from "../db/index.js";
2
+ import type { OpencodeClient } from "../lib/opencode.js";
3
+ export declare function createApp(sql: Sql, opencode: OpencodeClient, password: string): import("express-serve-static-core").Express;
@@ -0,0 +1,22 @@
1
+ import express from "express";
2
+ import { createRouter, createWorkerRouter } from "./routes.js";
3
+ function authMiddleware(password) {
4
+ return (req, res, next) => {
5
+ const header = req.headers.authorization;
6
+ if (!header || header !== `Bearer ${password}`) {
7
+ res.status(401).json({ error: "Unauthorized" });
8
+ return;
9
+ }
10
+ next();
11
+ };
12
+ }
13
+ export function createApp(sql, opencode, password) {
14
+ const app = express();
15
+ app.use(express.json());
16
+ // Worker routes are unauthenticated — mounted before auth middleware
17
+ app.use("/", createWorkerRouter(sql));
18
+ // Everything else requires Bearer auth
19
+ app.use(authMiddleware(password));
20
+ app.use("/", createRouter(sql, opencode));
21
+ return app;
22
+ }
@@ -0,0 +1,5 @@
1
+ import { Router } from "express";
2
+ import type { Sql } from "../db/index.js";
3
+ import type { OpencodeClient } from "../lib/opencode.js";
4
+ export declare function createRouter(sql: Sql, opencode: OpencodeClient): Router;
5
+ export declare function createWorkerRouter(sql: Sql): Router;