agent-office 0.0.9 → 0.0.11
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.
- package/dist/commands/communicator.js +6 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/migrate.js +7 -0
- package/dist/lib/opencode.d.ts +2 -2
- package/dist/lib/opencode.js +3 -2
- package/dist/manage/components/SessionList.js +8 -8
- package/dist/manage/hooks/useApi.d.ts +2 -2
- package/dist/manage/hooks/useApi.js +2 -2
- package/dist/server/cron.js +4 -9
- package/dist/server/routes.js +65 -203
- package/package.json +10 -11
|
@@ -429,13 +429,14 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
429
429
|
const input = document.getElementById('msg-input')
|
|
430
430
|
|
|
431
431
|
let lastSeenId = parseInt(document.querySelector('#messages-inner')?.dataset?.lastId ?? '0', 10)
|
|
432
|
+
let forceNextScroll = false
|
|
432
433
|
|
|
433
434
|
function scrollToBottom() {
|
|
434
|
-
if (outer) outer.scrollTop = outer.scrollHeight
|
|
435
|
+
if (outer) requestAnimationFrame(() => { outer.scrollTop = outer.scrollHeight })
|
|
435
436
|
}
|
|
436
437
|
|
|
437
438
|
function isNearBottom() {
|
|
438
|
-
return outer.scrollHeight - outer.scrollTop - outer.clientHeight <
|
|
439
|
+
return outer.scrollHeight - outer.scrollTop - outer.clientHeight < 150
|
|
439
440
|
}
|
|
440
441
|
|
|
441
442
|
// Auto-grow textarea
|
|
@@ -464,6 +465,7 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
464
465
|
input.focus()
|
|
465
466
|
// Force scroll on the next swap since we just sent a message
|
|
466
467
|
lastSeenId = -1
|
|
468
|
+
forceNextScroll = true
|
|
467
469
|
htmx.trigger(document.getElementById('messages'), 'load')
|
|
468
470
|
}
|
|
469
471
|
}
|
|
@@ -475,7 +477,8 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
475
477
|
const newLastId = parseInt(inner?.dataset?.lastId ?? '0', 10)
|
|
476
478
|
if (newLastId > lastSeenId) {
|
|
477
479
|
lastSeenId = newLastId
|
|
478
|
-
if (isNearBottom()) scrollToBottom()
|
|
480
|
+
if (forceNextScroll || isNearBottom()) scrollToBottom()
|
|
481
|
+
forceNextScroll = false
|
|
479
482
|
}
|
|
480
483
|
})
|
|
481
484
|
|
package/dist/db/index.d.ts
CHANGED
package/dist/db/migrate.js
CHANGED
|
@@ -93,6 +93,13 @@ const MIGRATIONS = [
|
|
|
93
93
|
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS status TEXT NULL;
|
|
94
94
|
`,
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
version: 8,
|
|
98
|
+
name: "rename_mode_to_agent",
|
|
99
|
+
sql: `
|
|
100
|
+
ALTER TABLE sessions RENAME COLUMN mode TO agent;
|
|
101
|
+
`,
|
|
102
|
+
},
|
|
96
103
|
];
|
|
97
104
|
export async function runMigrations(sql) {
|
|
98
105
|
await sql `
|
package/dist/lib/opencode.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
export type OpencodeClient =
|
|
1
|
+
import { createOpencodeClient as _createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
2
|
+
export type OpencodeClient = ReturnType<typeof _createOpencodeClient>;
|
|
3
3
|
export declare function createOpencodeClient(baseURL: string): OpencodeClient;
|
|
4
4
|
export interface OpencodeSession {
|
|
5
5
|
id: string;
|
package/dist/lib/opencode.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createOpencodeClient as _createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
2
|
+
import { cwd } from "process";
|
|
2
3
|
export function createOpencodeClient(baseURL) {
|
|
3
|
-
return
|
|
4
|
+
return _createOpencodeClient({ baseUrl: baseURL, directory: cwd() });
|
|
4
5
|
}
|
|
@@ -483,7 +483,7 @@ export function SessionList({ serverUrl, password, contentHeight, onSubViewChang
|
|
|
483
483
|
const [actionError, setActionError] = useState(null);
|
|
484
484
|
const [actionMsg, setActionMsg] = useState(null);
|
|
485
485
|
const [availableModes, setAvailableModes] = useState([]);
|
|
486
|
-
const [
|
|
486
|
+
const [pendingAgent, setPendingAgent] = useState(null);
|
|
487
487
|
const [modeCursor, setModeCursor] = useState(0);
|
|
488
488
|
const reload = () => void run(listSessions);
|
|
489
489
|
useEffect(() => { reload(); }, []);
|
|
@@ -522,7 +522,7 @@ export function SessionList({ serverUrl, password, contentHeight, onSubViewChang
|
|
|
522
522
|
if (input === "c") {
|
|
523
523
|
setActionError(null);
|
|
524
524
|
setActionMsg(null);
|
|
525
|
-
|
|
525
|
+
setPendingAgent(null);
|
|
526
526
|
setModeCursor(0);
|
|
527
527
|
setMode("creating-loading");
|
|
528
528
|
getModes().then((modes) => {
|
|
@@ -574,7 +574,7 @@ export function SessionList({ serverUrl, password, contentHeight, onSubViewChang
|
|
|
574
574
|
setModeCursor((c) => (c + 1) % total);
|
|
575
575
|
if (key.return) {
|
|
576
576
|
const selected = modeCursor === 0 ? null : (availableModes[modeCursor - 1]?.name ?? null);
|
|
577
|
-
|
|
577
|
+
setPendingAgent(selected);
|
|
578
578
|
setMode("creating-name");
|
|
579
579
|
}
|
|
580
580
|
if (key.escape) {
|
|
@@ -603,8 +603,8 @@ export function SessionList({ serverUrl, password, contentHeight, onSubViewChang
|
|
|
603
603
|
return;
|
|
604
604
|
setMode("creating-busy");
|
|
605
605
|
try {
|
|
606
|
-
await createSession(trimmed,
|
|
607
|
-
const modeNote =
|
|
606
|
+
await createSession(trimmed, pendingAgent ?? undefined);
|
|
607
|
+
const modeNote = pendingAgent ? ` [${pendingAgent}]` : "";
|
|
608
608
|
setActionMsg(`Coworker "${trimmed}"${modeNote} created.`);
|
|
609
609
|
setMode("create-done");
|
|
610
610
|
reload();
|
|
@@ -724,8 +724,8 @@ export function SessionList({ serverUrl, password, contentHeight, onSubViewChang
|
|
|
724
724
|
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" }) })] }));
|
|
725
725
|
}
|
|
726
726
|
if (mode === "creating-name") {
|
|
727
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Coworker" }),
|
|
728
|
-
? _jsxs(Text, { color: "yellow", children: ["[",
|
|
727
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Create Coworker" }), pendingAgent
|
|
728
|
+
? _jsxs(Text, { color: "yellow", children: ["[", pendingAgent, "]"] })
|
|
729
729
|
: _jsx(Text, { dimColor: true, children: "[no mode]" })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { placeholder: "e.g. alice", onSubmit: (v) => void handleCreate(v) })] }), _jsx(Text, { dimColor: true, children: "Enter to create \u00B7 Esc cancel" })] }));
|
|
730
730
|
}
|
|
731
731
|
if (mode === "creating-busy") {
|
|
@@ -793,7 +793,7 @@ export function SessionList({ serverUrl, password, contentHeight, onSubViewChang
|
|
|
793
793
|
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Coworkers" }), _jsxs(Text, { dimColor: true, children: ["(", rows.length, ")"] }), loading && _jsx(Spinner, {})] }), actionPanel, rows.length === 0 ? (_jsx(Box, { height: tableHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No coworkers yet. Press c to create one." }) })) : (_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: "STATUS".padEnd(20) }), _jsx(Text, { bold: true, color: "cyan", children: "MODE".padEnd(12) }), _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) => {
|
|
794
794
|
const selected = i === cursor;
|
|
795
795
|
const revealed = revealedRows.has(s.id);
|
|
796
|
-
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, { color: selected ? "cyan" : undefined, dimColor: !selected && !s.status, children: (s.status ?? "—").padEnd(20) }), _jsx(Text, { color: selected ? "magenta" : undefined, dimColor: !selected && !s.
|
|
796
|
+
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, { color: selected ? "cyan" : undefined, dimColor: !selected && !s.status, children: (s.status ?? "—").padEnd(20) }), _jsx(Text, { color: selected ? "magenta" : undefined, dimColor: !selected && !s.agent, children: (s.agent ?? "—").padEnd(12) }), _jsx(Text, { dimColor: !selected, children: s.session_id.padEnd(36) }), revealed
|
|
797
797
|
? _jsx(Text, { color: "yellow", children: s.agent_code })
|
|
798
798
|
: _jsx(Text, { dimColor: true, children: MASKED_CODE })] }, s.id));
|
|
799
799
|
})] }))] }));
|
|
@@ -3,7 +3,7 @@ export interface Session {
|
|
|
3
3
|
name: string;
|
|
4
4
|
session_id: string;
|
|
5
5
|
agent_code: string;
|
|
6
|
-
|
|
6
|
+
agent: string | null;
|
|
7
7
|
status: string | null;
|
|
8
8
|
created_at: string;
|
|
9
9
|
}
|
|
@@ -64,7 +64,7 @@ export interface MemoryRecord {
|
|
|
64
64
|
}
|
|
65
65
|
export declare function useApi(serverUrl: string, password: string): {
|
|
66
66
|
listSessions: () => Promise<Session[]>;
|
|
67
|
-
createSession: (name: string,
|
|
67
|
+
createSession: (name: string, agent?: string) => Promise<Session>;
|
|
68
68
|
deleteSession: (name: string) => Promise<void>;
|
|
69
69
|
checkHealth: () => Promise<boolean>;
|
|
70
70
|
getMessages: (name: string, limit?: number) => Promise<SessionMessage[]>;
|
|
@@ -28,10 +28,10 @@ export function useApi(serverUrl, password) {
|
|
|
28
28
|
const listSessions = useCallback(async () => {
|
|
29
29
|
return apiFetch(`${base}/sessions`, password);
|
|
30
30
|
}, [base, password]);
|
|
31
|
-
const createSession = useCallback(async (name,
|
|
31
|
+
const createSession = useCallback(async (name, agent) => {
|
|
32
32
|
return apiFetch(`${base}/sessions`, password, {
|
|
33
33
|
method: "POST",
|
|
34
|
-
body: JSON.stringify({ name, ...(
|
|
34
|
+
body: JSON.stringify({ name, ...(agent ? { agent } : {}) }),
|
|
35
35
|
});
|
|
36
36
|
}, [base, password]);
|
|
37
37
|
const getModes = useCallback(async () => {
|
package/dist/server/cron.js
CHANGED
|
@@ -59,21 +59,16 @@ export class CronScheduler {
|
|
|
59
59
|
const executedAt = new Date();
|
|
60
60
|
try {
|
|
61
61
|
const [session] = await this.sql `
|
|
62
|
-
SELECT session_id FROM sessions WHERE name = ${job.session_name}
|
|
62
|
+
SELECT session_id, agent FROM sessions WHERE name = ${job.session_name}
|
|
63
63
|
`;
|
|
64
64
|
if (!session) {
|
|
65
65
|
throw new Error(`Session "${job.session_name}" not found`);
|
|
66
66
|
}
|
|
67
|
-
const providers = await this.opencode.app.providers();
|
|
68
|
-
const defaultEntry = Object.entries(providers.default)[0];
|
|
69
|
-
if (!defaultEntry) {
|
|
70
|
-
throw new Error("No default model configured");
|
|
71
|
-
}
|
|
72
67
|
const injectText = `[Cron Job "${job.name}" — ${executedAt.toISOString()}]\n${job.message}${CRON_INJECTION_BLURB}`;
|
|
73
|
-
await this.opencode.session.
|
|
74
|
-
|
|
75
|
-
providerID: defaultEntry[1],
|
|
68
|
+
await this.opencode.session.promptAsync({
|
|
69
|
+
sessionID: session.session_id,
|
|
76
70
|
parts: [{ type: "text", text: injectText }],
|
|
71
|
+
...(session.agent ? { agent: session.agent } : {}),
|
|
77
72
|
});
|
|
78
73
|
await this.sql `
|
|
79
74
|
UPDATE cron_jobs SET last_run = ${executedAt} WHERE id = ${job.id}
|
package/dist/server/routes.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
-
import { cwd } from "process";
|
|
3
2
|
import { Cron as CronerInstance } from "croner";
|
|
4
3
|
const MAIL_INJECTION_BLURB = [
|
|
5
4
|
``,
|
|
@@ -17,7 +16,10 @@ const MAIL_INJECTION_BLURB = [
|
|
|
17
16
|
`Tip: For currency or prices, use code blocks. Example: put numbers in single or`,
|
|
18
17
|
`double quotes to preserve formatting characters like dollar signs.`,
|
|
19
18
|
].join("\n");
|
|
20
|
-
function
|
|
19
|
+
function generateEnrollmentMessage(token) {
|
|
20
|
+
return `You have been enrolled in the agent office.\n\nTo clock in and receive your full briefing, run:\n\n agent-office worker clock-in ${token}`;
|
|
21
|
+
}
|
|
22
|
+
function generateWelcomeMessage(name, agent, status, humanName, humanDescription, token) {
|
|
21
23
|
return [
|
|
22
24
|
`╔══════════════════════════════════════════════════════╗`,
|
|
23
25
|
`║ WELCOME TO THE AGENT OFFICE ║`,
|
|
@@ -25,7 +27,6 @@ function generateWelcomeMessage(name, mode, status, humanName, humanDescription,
|
|
|
25
27
|
``,
|
|
26
28
|
`You are now clocked in.`,
|
|
27
29
|
` Name: ${name}`,
|
|
28
|
-
...(mode ? [` Mode: ${mode}`] : []),
|
|
29
30
|
...(status ? [` Status: ${status}`] : []),
|
|
30
31
|
` Human manager: ${humanName} — the human who created your`,
|
|
31
32
|
` session, assigns your work, and is your`,
|
|
@@ -135,8 +136,8 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
135
136
|
});
|
|
136
137
|
router.get("/modes", async (_req, res) => {
|
|
137
138
|
try {
|
|
138
|
-
const config = await opencode.config.get();
|
|
139
|
-
const agent = config
|
|
139
|
+
const { data: config } = await opencode.config.get();
|
|
140
|
+
const agent = config?.agent ?? {};
|
|
140
141
|
const modes = Object.entries(agent)
|
|
141
142
|
.filter(([, val]) => val != null)
|
|
142
143
|
.map(([name, val]) => ({
|
|
@@ -154,7 +155,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
154
155
|
router.get("/sessions", async (_req, res) => {
|
|
155
156
|
try {
|
|
156
157
|
const rows = await sql `
|
|
157
|
-
SELECT id, name, session_id, agent_code,
|
|
158
|
+
SELECT id, name, session_id, agent_code, agent, status, created_at
|
|
158
159
|
FROM sessions
|
|
159
160
|
ORDER BY created_at DESC
|
|
160
161
|
`;
|
|
@@ -166,13 +167,13 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
166
167
|
}
|
|
167
168
|
});
|
|
168
169
|
router.post("/sessions", async (req, res) => {
|
|
169
|
-
const { name,
|
|
170
|
+
const { name, agent: agentArg } = req.body;
|
|
170
171
|
if (!name || typeof name !== "string" || !name.trim()) {
|
|
171
172
|
res.status(400).json({ error: "name is required" });
|
|
172
173
|
return;
|
|
173
174
|
}
|
|
174
175
|
const trimmedName = name.trim();
|
|
175
|
-
const
|
|
176
|
+
const trimmedAgent = typeof agentArg === "string" && agentArg.trim() ? agentArg.trim() : null;
|
|
176
177
|
const existing = await sql `
|
|
177
178
|
SELECT id FROM sessions WHERE name = ${trimmedName}
|
|
178
179
|
`;
|
|
@@ -182,7 +183,9 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
182
183
|
}
|
|
183
184
|
let opencodeSessionId;
|
|
184
185
|
try {
|
|
185
|
-
const session = await opencode.session.create(
|
|
186
|
+
const { data: session } = await opencode.session.create();
|
|
187
|
+
if (!session)
|
|
188
|
+
throw new Error("No session returned");
|
|
186
189
|
opencodeSessionId = session.id;
|
|
187
190
|
}
|
|
188
191
|
catch (err) {
|
|
@@ -193,34 +196,29 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
193
196
|
let row;
|
|
194
197
|
try {
|
|
195
198
|
const [inserted] = await sql `
|
|
196
|
-
INSERT INTO sessions (name, session_id,
|
|
197
|
-
VALUES (${trimmedName}, ${opencodeSessionId}, ${
|
|
198
|
-
RETURNING id, name, session_id, agent_code,
|
|
199
|
+
INSERT INTO sessions (name, session_id, agent)
|
|
200
|
+
VALUES (${trimmedName}, ${opencodeSessionId}, ${trimmedAgent})
|
|
201
|
+
RETURNING id, name, session_id, agent_code, agent, created_at
|
|
199
202
|
`;
|
|
200
203
|
row = inserted;
|
|
201
204
|
}
|
|
202
205
|
catch (err) {
|
|
203
206
|
console.error("DB insert error:", err);
|
|
204
207
|
try {
|
|
205
|
-
await opencode.session.delete(opencodeSessionId);
|
|
208
|
+
await opencode.session.delete({ sessionID: opencodeSessionId });
|
|
206
209
|
}
|
|
207
210
|
catch { }
|
|
208
211
|
res.status(500).json({ error: "Internal server error" });
|
|
209
212
|
return;
|
|
210
213
|
}
|
|
211
214
|
try {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
providerID: defaultEntry[1],
|
|
220
|
-
parts: [{ type: "text", text: enrollmentMessage }],
|
|
221
|
-
...(trimmedMode ? { mode: trimmedMode } : {}),
|
|
222
|
-
});
|
|
223
|
-
}
|
|
215
|
+
const clockInToken = `${row.agent_code}@${serverUrl}`;
|
|
216
|
+
const enrollmentMessage = generateEnrollmentMessage(clockInToken);
|
|
217
|
+
await opencode.session.promptAsync({
|
|
218
|
+
sessionID: opencodeSessionId,
|
|
219
|
+
parts: [{ type: "text", text: enrollmentMessage }],
|
|
220
|
+
...(trimmedAgent ? { agent: trimmedAgent } : {}),
|
|
221
|
+
});
|
|
224
222
|
}
|
|
225
223
|
catch (err) {
|
|
226
224
|
console.warn("Warning: could not send first message to session:", err);
|
|
@@ -262,8 +260,8 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
262
260
|
}
|
|
263
261
|
const row = rows[0];
|
|
264
262
|
try {
|
|
265
|
-
const messages = await opencode.session.messages(row.session_id);
|
|
266
|
-
const result = messages
|
|
263
|
+
const { data: messages } = await opencode.session.messages({ sessionID: row.session_id, limit });
|
|
264
|
+
const result = (messages ?? [])
|
|
267
265
|
.slice(-limit)
|
|
268
266
|
.map((m) => ({
|
|
269
267
|
role: m.info.role,
|
|
@@ -311,42 +309,22 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
311
309
|
return;
|
|
312
310
|
}
|
|
313
311
|
const row = rows[0];
|
|
314
|
-
let resolvedModelID = modelID;
|
|
315
|
-
let resolvedProviderID = providerID;
|
|
316
|
-
if (!resolvedModelID || !resolvedProviderID) {
|
|
317
|
-
try {
|
|
318
|
-
const providers = await opencode.app.providers();
|
|
319
|
-
const defaultEntry = Object.entries(providers.default)[0];
|
|
320
|
-
if (!defaultEntry) {
|
|
321
|
-
res.status(502).json({ error: "No default model configured in OpenCode" });
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
resolvedModelID = resolvedModelID ?? defaultEntry[0];
|
|
325
|
-
resolvedProviderID = resolvedProviderID ?? defaultEntry[1];
|
|
326
|
-
}
|
|
327
|
-
catch (err) {
|
|
328
|
-
console.error("OpenCode app.providers error:", err);
|
|
329
|
-
res.status(502).json({ error: "Failed to fetch providers from OpenCode", detail: String(err) });
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
312
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
providerID: resolvedProviderID,
|
|
313
|
+
await opencode.session.promptAsync({
|
|
314
|
+
sessionID: row.session_id,
|
|
337
315
|
parts: [{ type: "text", text: text.trim() }],
|
|
338
316
|
});
|
|
339
|
-
res.json({ ok: true
|
|
317
|
+
res.json({ ok: true });
|
|
340
318
|
}
|
|
341
319
|
catch (err) {
|
|
342
|
-
console.error("OpenCode session.
|
|
320
|
+
console.error("OpenCode session.prompt error:", err);
|
|
343
321
|
res.status(502).json({ error: "Failed to inject message into OpenCode session", detail: String(err) });
|
|
344
322
|
}
|
|
345
323
|
});
|
|
346
324
|
router.post("/sessions/:name/revert-to-start", async (req, res) => {
|
|
347
325
|
const { name } = req.params;
|
|
348
326
|
const rows = await sql `
|
|
349
|
-
SELECT id, name, session_id, agent_code,
|
|
327
|
+
SELECT id, name, session_id, agent_code, agent, status, created_at FROM sessions WHERE name = ${name}
|
|
350
328
|
`;
|
|
351
329
|
if (rows.length === 0) {
|
|
352
330
|
res.status(404).json({ error: `Session "${name}" not found` });
|
|
@@ -354,8 +332,8 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
354
332
|
}
|
|
355
333
|
const session = rows[0];
|
|
356
334
|
try {
|
|
357
|
-
const messages = await opencode.session.messages(session.session_id);
|
|
358
|
-
if (messages.length === 0) {
|
|
335
|
+
const { data: messages } = await opencode.session.messages({ sessionID: session.session_id });
|
|
336
|
+
if (!messages || messages.length === 0) {
|
|
359
337
|
res.status(400).json({ error: "Session has no messages to revert to" });
|
|
360
338
|
return;
|
|
361
339
|
}
|
|
@@ -366,20 +344,14 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
366
344
|
}
|
|
367
345
|
// Abort any in-progress generation before reverting, to avoid "session is busy" errors.
|
|
368
346
|
// Swallow errors — the session may not be busy, in which case abort is a no-op.
|
|
369
|
-
await opencode.session.abort(session.session_id).catch(() => { });
|
|
370
|
-
await opencode.session.revert(session.session_id,
|
|
371
|
-
const providers = await opencode.app.providers();
|
|
372
|
-
const defaultEntry = Object.entries(providers.default)[0];
|
|
373
|
-
if (!defaultEntry) {
|
|
374
|
-
res.status(502).json({ error: "No default model configured in OpenCode" });
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
347
|
+
await opencode.session.abort({ sessionID: session.session_id }).catch(() => { });
|
|
348
|
+
await opencode.session.revert({ sessionID: session.session_id, messageID: firstMessage.info.id });
|
|
377
349
|
const clockInToken = `${session.agent_code}@${serverUrl}`;
|
|
378
|
-
const enrollmentMessage =
|
|
379
|
-
await opencode.session.
|
|
380
|
-
|
|
381
|
-
providerID: defaultEntry[1],
|
|
350
|
+
const enrollmentMessage = generateEnrollmentMessage(clockInToken);
|
|
351
|
+
await opencode.session.promptAsync({
|
|
352
|
+
sessionID: session.session_id,
|
|
382
353
|
parts: [{ type: "text", text: enrollmentMessage }],
|
|
354
|
+
...(session.agent ? { agent: session.agent } : {}),
|
|
383
355
|
});
|
|
384
356
|
res.json({ ok: true, messageID: firstMessage.info.id });
|
|
385
357
|
}
|
|
@@ -390,19 +362,13 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
390
362
|
});
|
|
391
363
|
router.post("/sessions/revert-all", async (_req, res) => {
|
|
392
364
|
const allSessions = await sql `
|
|
393
|
-
SELECT id, name, session_id, agent_code,
|
|
365
|
+
SELECT id, name, session_id, agent_code, agent, status, created_at FROM sessions
|
|
394
366
|
`;
|
|
395
|
-
const providers = await opencode.app.providers();
|
|
396
|
-
const defaultEntry = Object.entries(providers.default)[0];
|
|
397
|
-
if (!defaultEntry) {
|
|
398
|
-
res.status(502).json({ error: "No default model configured in OpenCode" });
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
367
|
const results = [];
|
|
402
368
|
for (const session of allSessions) {
|
|
403
369
|
try {
|
|
404
|
-
const messages = await opencode.session.messages(session.session_id);
|
|
405
|
-
if (messages.length === 0) {
|
|
370
|
+
const { data: messages } = await opencode.session.messages({ sessionID: session.session_id });
|
|
371
|
+
if (!messages || messages.length === 0) {
|
|
406
372
|
results.push({ name: session.name, ok: false, error: "No messages" });
|
|
407
373
|
continue;
|
|
408
374
|
}
|
|
@@ -412,14 +378,14 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
412
378
|
continue;
|
|
413
379
|
}
|
|
414
380
|
// Abort any in-progress generation before reverting
|
|
415
|
-
await opencode.session.abort(session.session_id).catch(() => { });
|
|
416
|
-
await opencode.session.revert(session.session_id,
|
|
381
|
+
await opencode.session.abort({ sessionID: session.session_id }).catch(() => { });
|
|
382
|
+
await opencode.session.revert({ sessionID: session.session_id, messageID: firstMessage.info.id });
|
|
417
383
|
const clockInToken = `${session.agent_code}@${serverUrl}`;
|
|
418
|
-
const enrollmentMessage =
|
|
419
|
-
await opencode.session.
|
|
420
|
-
|
|
421
|
-
providerID: defaultEntry[1],
|
|
384
|
+
const enrollmentMessage = generateEnrollmentMessage(clockInToken);
|
|
385
|
+
await opencode.session.promptAsync({
|
|
386
|
+
sessionID: session.session_id,
|
|
422
387
|
parts: [{ type: "text", text: enrollmentMessage }],
|
|
388
|
+
...(session.agent ? { agent: session.agent } : {}),
|
|
423
389
|
});
|
|
424
390
|
results.push({ name: session.name, ok: true });
|
|
425
391
|
}
|
|
@@ -442,7 +408,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
442
408
|
}
|
|
443
409
|
const row = rows[0];
|
|
444
410
|
try {
|
|
445
|
-
await opencode.session.delete(row.session_id);
|
|
411
|
+
await opencode.session.delete({ sessionID: row.session_id });
|
|
446
412
|
}
|
|
447
413
|
catch (err) {
|
|
448
414
|
console.error("OpenCode session.delete error:", err);
|
|
@@ -565,8 +531,6 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
565
531
|
res.status(400).json({ error: "No valid recipients found" });
|
|
566
532
|
return;
|
|
567
533
|
}
|
|
568
|
-
const providers = await opencode.app.providers();
|
|
569
|
-
const defaultEntry = Object.entries(providers.default)[0];
|
|
570
534
|
const results = [];
|
|
571
535
|
for (const recipient of validRecipients) {
|
|
572
536
|
const [msgRow] = await sql `
|
|
@@ -576,13 +540,12 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
576
540
|
`;
|
|
577
541
|
const msgId = msgRow.id;
|
|
578
542
|
let injected = false;
|
|
579
|
-
if (sessionMap.has(recipient)
|
|
543
|
+
if (sessionMap.has(recipient)) {
|
|
580
544
|
const sessionId = sessionMap.get(recipient);
|
|
581
545
|
const injectText = `[Message from "${trimmedFrom}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
|
|
582
546
|
try {
|
|
583
|
-
await opencode.session.
|
|
584
|
-
|
|
585
|
-
providerID: defaultEntry[1],
|
|
547
|
+
await opencode.session.promptAsync({
|
|
548
|
+
sessionID: sessionId,
|
|
586
549
|
parts: [{ type: "text", text: injectText }],
|
|
587
550
|
});
|
|
588
551
|
await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`;
|
|
@@ -597,7 +560,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
597
560
|
res.status(201).json({ ok: true, results });
|
|
598
561
|
});
|
|
599
562
|
router.post("/messages/:id/read", async (req, res) => {
|
|
600
|
-
const id = parseInt(req.params.id, 10);
|
|
563
|
+
const id = parseInt(String(req.params.id), 10);
|
|
601
564
|
if (isNaN(id)) {
|
|
602
565
|
res.status(400).json({ error: "Invalid message id" });
|
|
603
566
|
return;
|
|
@@ -738,7 +701,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
738
701
|
}
|
|
739
702
|
});
|
|
740
703
|
router.delete("/crons/:id", async (req, res) => {
|
|
741
|
-
const id = parseInt(req.params.id, 10);
|
|
704
|
+
const id = parseInt(String(req.params.id), 10);
|
|
742
705
|
if (isNaN(id)) {
|
|
743
706
|
res.status(400).json({ error: "Invalid cron job id" });
|
|
744
707
|
return;
|
|
@@ -759,7 +722,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
759
722
|
}
|
|
760
723
|
});
|
|
761
724
|
router.post("/crons/:id/enable", async (req, res) => {
|
|
762
|
-
const id = parseInt(req.params.id, 10);
|
|
725
|
+
const id = parseInt(String(req.params.id), 10);
|
|
763
726
|
if (isNaN(id)) {
|
|
764
727
|
res.status(400).json({ error: "Invalid cron job id" });
|
|
765
728
|
return;
|
|
@@ -809,7 +772,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
809
772
|
}
|
|
810
773
|
});
|
|
811
774
|
router.post("/crons/:id/disable", async (req, res) => {
|
|
812
|
-
const id = parseInt(req.params.id, 10);
|
|
775
|
+
const id = parseInt(String(req.params.id), 10);
|
|
813
776
|
if (isNaN(id)) {
|
|
814
777
|
res.status(400).json({ error: "Invalid cron job id" });
|
|
815
778
|
return;
|
|
@@ -841,7 +804,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
|
|
|
841
804
|
}
|
|
842
805
|
});
|
|
843
806
|
router.get("/crons/:id/history", async (req, res) => {
|
|
844
|
-
const id = parseInt(req.params.id, 10);
|
|
807
|
+
const id = parseInt(String(req.params.id), 10);
|
|
845
808
|
const limit = Math.min(parseInt(req.query.limit ?? "10", 10), 100);
|
|
846
809
|
if (isNaN(id)) {
|
|
847
810
|
res.status(400).json({ error: "Invalid cron job id" });
|
|
@@ -1012,109 +975,11 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1012
975
|
}
|
|
1013
976
|
const session = rows[0];
|
|
1014
977
|
const token = `${session.agent_code}@<server-url>`;
|
|
1015
|
-
const name = session.name;
|
|
1016
978
|
const humanConfig = await sql `SELECT value FROM config WHERE key = 'human_name'`;
|
|
1017
979
|
const humanName = humanConfig[0]?.value ?? "your human manager";
|
|
1018
980
|
const humanDescConfig = await sql `SELECT value FROM config WHERE key = 'human_description'`;
|
|
1019
981
|
const humanDescription = humanDescConfig[0]?.value ?? "";
|
|
1020
|
-
const message =
|
|
1021
|
-
`╔══════════════════════════════════════════════════════╗`,
|
|
1022
|
-
`║ WELCOME TO THE AGENT OFFICE ║`,
|
|
1023
|
-
`╚══════════════════════════════════════════════════════╝`,
|
|
1024
|
-
``,
|
|
1025
|
-
`You are now clocked in.`,
|
|
1026
|
-
` Name: ${name}`,
|
|
1027
|
-
...(session.mode ? [` Mode: ${session.mode}`] : []),
|
|
1028
|
-
` Human manager: ${humanName} — the human who created your`,
|
|
1029
|
-
` session, assigns your work, and is your`,
|
|
1030
|
-
` primary point of contact for questions,`,
|
|
1031
|
-
` updates, and decisions.`,
|
|
1032
|
-
...(humanDescription ? [
|
|
1033
|
-
` "${humanDescription}"`,
|
|
1034
|
-
] : []),
|
|
1035
|
-
``,
|
|
1036
|
-
`The agent-office CLI is your PRIMARY means of communicating`,
|
|
1037
|
-
`with your human manager (${humanName}) and your coworkers.`,
|
|
1038
|
-
`Use it to send and receive messages, and to discover who`,
|
|
1039
|
-
`else is working.`,
|
|
1040
|
-
``,
|
|
1041
|
-
`════════════════════════════════════════════════════════`,
|
|
1042
|
-
` AVAILABLE COMMANDS`,
|
|
1043
|
-
`════════════════════════════════════════════════════════`,
|
|
1044
|
-
``,
|
|
1045
|
-
` List your coworkers`,
|
|
1046
|
-
` agent-office worker list-coworkers \\`,
|
|
1047
|
-
` ${token}`,
|
|
1048
|
-
``,
|
|
1049
|
-
` Send a message to your manager or a coworker`,
|
|
1050
|
-
` agent-office worker send-message \\`,
|
|
1051
|
-
` --name <recipient-name> \\`,
|
|
1052
|
-
` --body "Your message here" \\`,
|
|
1053
|
-
` ${token}`,
|
|
1054
|
-
``,
|
|
1055
|
-
` Send a message to multiple recipients at once`,
|
|
1056
|
-
` agent-office worker send-message \\`,
|
|
1057
|
-
` --name alice --name bob \\`,
|
|
1058
|
-
` --body "Your message here" \\`,
|
|
1059
|
-
` ${token}`,
|
|
1060
|
-
``,
|
|
1061
|
-
` Manage scheduled tasks (optional)`,
|
|
1062
|
-
` agent-office worker cron \\`,
|
|
1063
|
-
` ${token}`,
|
|
1064
|
-
``,
|
|
1065
|
-
` Store a memory (persistent across sessions)`,
|
|
1066
|
-
` agent-office worker memory add \\`,
|
|
1067
|
-
` --content "your memory here" \\`,
|
|
1068
|
-
` ${token}`,
|
|
1069
|
-
``,
|
|
1070
|
-
` Search your memories`,
|
|
1071
|
-
` agent-office worker memory search \\`,
|
|
1072
|
-
` --query "your search" \\`,
|
|
1073
|
-
` ${token}`,
|
|
1074
|
-
``,
|
|
1075
|
-
` List all your memories`,
|
|
1076
|
-
` agent-office worker memory list \\`,
|
|
1077
|
-
` ${token}`,
|
|
1078
|
-
``,
|
|
1079
|
-
` Forget a memory`,
|
|
1080
|
-
` agent-office worker memory forget \\`,
|
|
1081
|
-
` ${token} <memory-id>`,
|
|
1082
|
-
``,
|
|
1083
|
-
`════════════════════════════════════════════════════════`,
|
|
1084
|
-
` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
|
|
1085
|
-
`════════════════════════════════════════════════════════`,
|
|
1086
|
-
``,
|
|
1087
|
-
` Nobody — not ${humanName}, not your coworkers — can see`,
|
|
1088
|
-
` anything you think, reason, or write inside this session.`,
|
|
1089
|
-
` Your work is completely private until you explicitly send`,
|
|
1090
|
-
` a message using the send-message command.`,
|
|
1091
|
-
``,
|
|
1092
|
-
` This means:`,
|
|
1093
|
-
` • You must send a message to report progress or ask`,
|
|
1094
|
-
` for help. Silence looks like inaction.`,
|
|
1095
|
-
` • Finishing a task without messaging ${humanName}`,
|
|
1096
|
-
` means they will never know it is done.`,
|
|
1097
|
-
` • If you are blocked or uncertain, send a message —`,
|
|
1098
|
-
` nobody will know otherwise.`,
|
|
1099
|
-
``,
|
|
1100
|
-
`════════════════════════════════════════════════════════`,
|
|
1101
|
-
` TIPS`,
|
|
1102
|
-
`════════════════════════════════════════════════════════`,
|
|
1103
|
-
``,
|
|
1104
|
-
` - Run list-coworkers to discover who is available and`,
|
|
1105
|
-
` what their names are before sending messages.`,
|
|
1106
|
-
` - Messages you send are delivered directly into the`,
|
|
1107
|
-
` recipient's active session — they will see them`,
|
|
1108
|
-
` immediately.`,
|
|
1109
|
-
` - Your human manager is ${humanName}. They can send you`,
|
|
1110
|
-
` messages at any time and those will appear here in`,
|
|
1111
|
-
` your session just like this one. You can reach them`,
|
|
1112
|
-
` by sending a message to --name ${humanName}.`,
|
|
1113
|
-
` - Optional: Set up recurring scheduled tasks with cron`,
|
|
1114
|
-
` jobs. Run 'agent-office worker cron list ${token}' to`,
|
|
1115
|
-
` get started.`,
|
|
1116
|
-
``,
|
|
1117
|
-
].join("\n");
|
|
982
|
+
const message = generateWelcomeMessage(session.name, session.agent, session.status ?? null, humanName, humanDescription, token);
|
|
1118
983
|
res.json({
|
|
1119
984
|
ok: true,
|
|
1120
985
|
name: session.name,
|
|
@@ -1164,7 +1029,7 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1164
1029
|
return;
|
|
1165
1030
|
}
|
|
1166
1031
|
const rows = await sql `
|
|
1167
|
-
SELECT id, name, session_id, agent_code,
|
|
1032
|
+
SELECT id, name, session_id, agent_code, agent, status, created_at
|
|
1168
1033
|
FROM sessions
|
|
1169
1034
|
WHERE agent_code = ${code}
|
|
1170
1035
|
`;
|
|
@@ -1230,8 +1095,6 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1230
1095
|
res.status(400).json({ error: "No valid recipients found" });
|
|
1231
1096
|
return;
|
|
1232
1097
|
}
|
|
1233
|
-
const providers = await opencode.app.providers();
|
|
1234
|
-
const defaultEntry = Object.entries(providers.default)[0];
|
|
1235
1098
|
const results = [];
|
|
1236
1099
|
for (const recipient of validRecipients) {
|
|
1237
1100
|
const [msgRow] = await sql `
|
|
@@ -1241,13 +1104,12 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1241
1104
|
`;
|
|
1242
1105
|
const msgId = msgRow.id;
|
|
1243
1106
|
let injected = false;
|
|
1244
|
-
if (sessionMap.has(recipient)
|
|
1107
|
+
if (sessionMap.has(recipient)) {
|
|
1245
1108
|
const recipientSessionId = sessionMap.get(recipient);
|
|
1246
1109
|
const injectText = `[Message from "${session.name}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
|
|
1247
1110
|
try {
|
|
1248
|
-
await opencode.session.
|
|
1249
|
-
|
|
1250
|
-
providerID: defaultEntry[1],
|
|
1111
|
+
await opencode.session.promptAsync({
|
|
1112
|
+
sessionID: recipientSessionId,
|
|
1251
1113
|
parts: [{ type: "text", text: injectText }],
|
|
1252
1114
|
});
|
|
1253
1115
|
await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`;
|
|
@@ -1397,7 +1259,7 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1397
1259
|
});
|
|
1398
1260
|
router.delete("/worker/crons/:id", async (req, res) => {
|
|
1399
1261
|
const { code } = req.query;
|
|
1400
|
-
const id = parseInt(req.params.id, 10);
|
|
1262
|
+
const id = parseInt(String(req.params.id), 10);
|
|
1401
1263
|
if (!code || typeof code !== "string") {
|
|
1402
1264
|
res.status(400).json({ error: "code query parameter is required" });
|
|
1403
1265
|
return;
|
|
@@ -1432,7 +1294,7 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1432
1294
|
});
|
|
1433
1295
|
router.post("/worker/crons/:id/enable", async (req, res) => {
|
|
1434
1296
|
const { code } = req.query;
|
|
1435
|
-
const id = parseInt(req.params.id, 10);
|
|
1297
|
+
const id = parseInt(String(req.params.id), 10);
|
|
1436
1298
|
if (!code || typeof code !== "string") {
|
|
1437
1299
|
res.status(400).json({ error: "code query parameter is required" });
|
|
1438
1300
|
return;
|
|
@@ -1494,7 +1356,7 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1494
1356
|
});
|
|
1495
1357
|
router.post("/worker/crons/:id/disable", async (req, res) => {
|
|
1496
1358
|
const { code } = req.query;
|
|
1497
|
-
const id = parseInt(req.params.id, 10);
|
|
1359
|
+
const id = parseInt(String(req.params.id), 10);
|
|
1498
1360
|
if (!code || typeof code !== "string") {
|
|
1499
1361
|
res.status(400).json({ error: "code query parameter is required" });
|
|
1500
1362
|
return;
|
|
@@ -1538,7 +1400,7 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
|
1538
1400
|
});
|
|
1539
1401
|
router.get("/worker/crons/:id/history", async (req, res) => {
|
|
1540
1402
|
const { code } = req.query;
|
|
1541
|
-
const id = parseInt(req.params.id, 10);
|
|
1403
|
+
const id = parseInt(String(req.params.id), 10);
|
|
1542
1404
|
const limit = Math.min(parseInt(req.query.limit ?? "10", 10), 100);
|
|
1543
1405
|
if (!code || typeof code !== "string") {
|
|
1544
1406
|
res.status(400).json({ error: "code query parameter is required" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-office",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Manage OpenCode sessions with named aliases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,22 +33,21 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@inkjs/ui": "^2.0.0",
|
|
36
|
-
"@opencode-ai/sdk": "^
|
|
37
|
-
"
|
|
38
|
-
"commander": "^12.0.0",
|
|
36
|
+
"@opencode-ai/sdk": "^1.2.10",
|
|
37
|
+
"commander": "^14.0.0",
|
|
39
38
|
"croner": "^10.0.1",
|
|
40
|
-
"dotenv": "^
|
|
41
|
-
"express": "^
|
|
39
|
+
"dotenv": "^17.0.0",
|
|
40
|
+
"express": "^5.0.0",
|
|
42
41
|
"fastmemory": "^0.1.5",
|
|
43
|
-
"ink": "^
|
|
42
|
+
"ink": "^6.0.0",
|
|
44
43
|
"postgres": "^3.4.0",
|
|
45
|
-
"react": "^
|
|
44
|
+
"react": "^19.0.0"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@types/better-sqlite3": "^7.6.13",
|
|
49
|
-
"@types/express": "^
|
|
50
|
-
"@types/node": "^
|
|
51
|
-
"@types/react": "^
|
|
48
|
+
"@types/express": "^5.0.0",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"@types/react": "^19.0.0",
|
|
52
51
|
"tsx": "^4.0.0",
|
|
53
52
|
"typescript": "^5.0.0"
|
|
54
53
|
}
|