agent-office 0.0.2 → 0.0.3
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/cli.js +20 -0
- package/dist/commands/worker.d.ts +1 -0
- package/dist/commands/worker.js +4 -0
- package/dist/db/index.d.ts +1 -0
- package/dist/db/migrate.js +7 -0
- package/dist/manage/app.js +1 -1
- package/dist/manage/components/SessionList.js +87 -7
- package/dist/manage/components/SessionSidebar.js +7 -1
- package/dist/manage/components/TailMessages.js +54 -5
- package/dist/manage/hooks/useApi.d.ts +11 -2
- package/dist/manage/hooks/useApi.js +4 -0
- package/dist/server/routes.js +205 -12
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,26 @@ workerCmd
|
|
|
46
46
|
const { listCoworkers } = await import("./commands/worker.js");
|
|
47
47
|
await listCoworkers(token);
|
|
48
48
|
});
|
|
49
|
+
workerCmd
|
|
50
|
+
.command("set-status")
|
|
51
|
+
.description("Set your public status (visible to coworkers and manager)")
|
|
52
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
53
|
+
.option("--status <status>", "Your status message (max 140 characters)")
|
|
54
|
+
.option("--clear", "Clear your status")
|
|
55
|
+
.action(async (token, options) => {
|
|
56
|
+
if (options.clear) {
|
|
57
|
+
const { setStatus } = await import("./commands/worker.js");
|
|
58
|
+
await setStatus(token, null);
|
|
59
|
+
}
|
|
60
|
+
else if (options.status !== undefined) {
|
|
61
|
+
const { setStatus } = await import("./commands/worker.js");
|
|
62
|
+
await setStatus(token, options.status);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.error("Error: either --status or --clear must be provided");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
49
69
|
workerCmd
|
|
50
70
|
.command("send-message")
|
|
51
71
|
.description("Send a message to one or more coworkers")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function clockIn(token: string): Promise<void>;
|
|
2
2
|
export declare function listCoworkers(token: string): Promise<void>;
|
|
3
|
+
export declare function setStatus(token: string, status: string | null): Promise<void>;
|
|
3
4
|
export declare function sendMessage(token: string, recipients: string[], body: string): Promise<void>;
|
|
4
5
|
export declare function listCrons(token: string): Promise<void>;
|
|
5
6
|
export declare function createCron(token: string, options: {
|
package/dist/commands/worker.js
CHANGED
|
@@ -112,6 +112,10 @@ export async function listCoworkers(token) {
|
|
|
112
112
|
const workers = await fetchWorker(token, "/worker/list-coworkers");
|
|
113
113
|
console.log(JSON.stringify(workers, null, 2));
|
|
114
114
|
}
|
|
115
|
+
export async function setStatus(token, status) {
|
|
116
|
+
const result = await postWorker(token, "/worker/set-status", { status });
|
|
117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
118
|
+
}
|
|
115
119
|
export async function sendMessage(token, recipients, body) {
|
|
116
120
|
const result = await postWorker(token, "/worker/send-message", { to: recipients, body });
|
|
117
121
|
console.log(JSON.stringify(result, null, 2));
|
package/dist/db/index.d.ts
CHANGED
package/dist/db/migrate.js
CHANGED
|
@@ -86,6 +86,13 @@ const MIGRATIONS = [
|
|
|
86
86
|
CREATE INDEX IF NOT EXISTS idx_cron_history_job_id ON cron_history(cron_job_id);
|
|
87
87
|
`,
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
version: 7,
|
|
91
|
+
name: "add_status_to_sessions",
|
|
92
|
+
sql: `
|
|
93
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS status TEXT NULL;
|
|
94
|
+
`,
|
|
95
|
+
},
|
|
89
96
|
];
|
|
90
97
|
export async function runMigrations(sql) {
|
|
91
98
|
await sql `
|
package/dist/manage/app.js
CHANGED
|
@@ -23,7 +23,7 @@ const FOOTER_HINTS = {
|
|
|
23
23
|
connecting: "",
|
|
24
24
|
"auth-error": "",
|
|
25
25
|
menu: "↑↓ navigate · Enter select · q quit",
|
|
26
|
-
list: "↑↓ navigate · c create · d delete · r reveal code · g regen · t tail · i inject · m mail · Esc back",
|
|
26
|
+
list: "↑↓ navigate · c create · d delete · r reveal code · g regen · x revert · t tail · i inject · m mail · Esc back",
|
|
27
27
|
"send-message": "Enter submit · Esc back to menu",
|
|
28
28
|
"my-mail": "↑↓ select message · r reply · m mark read · a mark all read · s sent tab · Esc back",
|
|
29
29
|
"profile": "Enter submit · Esc back to menu",
|
|
@@ -34,11 +34,60 @@ function TailView({ serverUrl, password, sessionName, contentHeight, onClose })
|
|
|
34
34
|
const messageLines = [];
|
|
35
35
|
for (const msg of messages) {
|
|
36
36
|
for (const part of msg.parts) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
if (part.type === "text" && part.text) {
|
|
38
|
+
const lines = part.text.split("\n");
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
messageLines.push({ role: msg.role, text: i === 0 ? lines[i] : ` ${lines[i]}` });
|
|
41
|
+
}
|
|
42
|
+
messageLines.push({ role: msg.role, text: "" });
|
|
43
|
+
}
|
|
44
|
+
else if (part.type === "tool") {
|
|
45
|
+
// Tool part has: tool (string), input, output
|
|
46
|
+
const toolName = typeof part.tool === "string" ? part.tool : String(part.tool ?? "unknown");
|
|
47
|
+
messageLines.push({ role: msg.role, text: "" }); // Blank line before tool
|
|
48
|
+
messageLines.push({ role: msg.role, text: `▶ Tool: ${toolName}` });
|
|
49
|
+
if (part.input !== undefined) {
|
|
50
|
+
// Try to pretty-print input if it's an object
|
|
51
|
+
let inputStr;
|
|
52
|
+
if (typeof part.input === "object" && part.input !== null) {
|
|
53
|
+
inputStr = JSON.stringify(part.input, null, 2);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
inputStr = String(part.input);
|
|
57
|
+
}
|
|
58
|
+
const lines = inputStr.split("\n");
|
|
59
|
+
for (let i = 0; i < Math.min(lines.length, 15); i++) {
|
|
60
|
+
messageLines.push({ role: msg.role, text: ` ${lines[i]}` });
|
|
61
|
+
}
|
|
62
|
+
if (lines.length > 15) {
|
|
63
|
+
messageLines.push({ role: msg.role, text: ` [...] (${lines.length - 15} more lines)` });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (part.output !== undefined) {
|
|
67
|
+
// Show output type and preview
|
|
68
|
+
let outputPreview;
|
|
69
|
+
if (typeof part.output === "object" && part.output !== null) {
|
|
70
|
+
outputPreview = JSON.stringify(part.output, null, 2).slice(0, 200);
|
|
71
|
+
}
|
|
72
|
+
else if (typeof part.output === "string") {
|
|
73
|
+
outputPreview = part.output.slice(0, 200);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
outputPreview = String(part.output).slice(0, 200);
|
|
77
|
+
}
|
|
78
|
+
const outputType = typeof part.output === "object" ? "object" : typeof part.output;
|
|
79
|
+
messageLines.push({ role: msg.role, text: ` ${"—".repeat(40)}` });
|
|
80
|
+
messageLines.push({ role: msg.role, text: ` Result (${outputType}):` });
|
|
81
|
+
const lines = outputPreview.split("\n");
|
|
82
|
+
for (let i = 0; i < Math.min(lines.length, 5); i++) {
|
|
83
|
+
messageLines.push({ role: msg.role, text: ` ${lines[i]}` });
|
|
84
|
+
}
|
|
85
|
+
if (lines.length > 5 || outputPreview.length >= 200) {
|
|
86
|
+
messageLines.push({ role: msg.role, text: " [...]" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
messageLines.push({ role: msg.role, text: "" }); // Blank line after tool
|
|
40
90
|
}
|
|
41
|
-
messageLines.push({ role: msg.role, text: "" });
|
|
42
91
|
}
|
|
43
92
|
}
|
|
44
93
|
const viewHeight = contentHeight - 3;
|
|
@@ -164,7 +213,7 @@ function CoworkerMailView({ serverUrl, password, sessionName, contentHeight, onC
|
|
|
164
213
|
}
|
|
165
214
|
// ─── Main component ──────────────────────────────────────────────────────────
|
|
166
215
|
export function SessionList({ serverUrl, password, contentHeight }) {
|
|
167
|
-
const { listSessions, createSession, deleteSession, regenerateCode, getModes } = useApi(serverUrl, password);
|
|
216
|
+
const { listSessions, createSession, deleteSession, regenerateCode, getModes, revertToStart } = useApi(serverUrl, password);
|
|
168
217
|
const { data: sessions, loading, error: loadError, run } = useAsyncState();
|
|
169
218
|
const [cursor, setCursor] = useState(0);
|
|
170
219
|
const [revealedRows, setRevealedRows] = useState(new Set());
|
|
@@ -229,6 +278,11 @@ export function SessionList({ serverUrl, password, contentHeight }) {
|
|
|
229
278
|
setActionMsg(null);
|
|
230
279
|
setMode("confirm-regen");
|
|
231
280
|
}
|
|
281
|
+
if (input === "x" && rows.length > 0) {
|
|
282
|
+
setActionError(null);
|
|
283
|
+
setActionMsg(null);
|
|
284
|
+
setMode("confirm-revert");
|
|
285
|
+
}
|
|
232
286
|
if (rows.length > 0) {
|
|
233
287
|
if (input === "t")
|
|
234
288
|
setSubView("tail");
|
|
@@ -333,6 +387,28 @@ export function SessionList({ serverUrl, password, contentHeight }) {
|
|
|
333
387
|
setMode("create-error");
|
|
334
388
|
}
|
|
335
389
|
};
|
|
390
|
+
const handleConfirmRevert = async (confirmed) => {
|
|
391
|
+
if (!confirmed) {
|
|
392
|
+
setMode("browse");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const target = rows[cursor];
|
|
396
|
+
if (!target) {
|
|
397
|
+
setMode("browse");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
setMode("reverting");
|
|
401
|
+
try {
|
|
402
|
+
await revertToStart(target.name);
|
|
403
|
+
setActionMsg(`Session "${target.name}" reverted to first message and restarted.`);
|
|
404
|
+
setMode("create-done");
|
|
405
|
+
reload();
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
setActionError(err instanceof Error ? err.message : String(err));
|
|
409
|
+
setMode("create-error");
|
|
410
|
+
}
|
|
411
|
+
};
|
|
336
412
|
// ── Full-screen create flow ───────────────────────────────────────────────
|
|
337
413
|
if (mode === "creating-loading") {
|
|
338
414
|
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading agent configs..." }) }));
|
|
@@ -397,16 +473,20 @@ export function SessionList({ serverUrl, password, contentHeight }) {
|
|
|
397
473
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Regenerate Agent Code" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Regenerate code for ", _jsx(Text, { color: "cyan", bold: true, children: target.name }), "?", " ", _jsx(Text, { dimColor: true, children: "The old code will stop working immediately." })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleConfirmRegen(true), onCancel: () => void handleConfirmRegen(false) }) })] }));
|
|
398
474
|
if (mode === "regenerating")
|
|
399
475
|
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: "Generating new agent code..." }) }));
|
|
476
|
+
if (mode === "confirm-revert" && target)
|
|
477
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Revert to First Message" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Revert ", _jsx(Text, { color: "cyan", bold: true, children: target.name }), " to its first message and restart?", " ", _jsx(Text, { dimColor: true, children: "This clears all messages after the first one." })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleConfirmRevert(true), onCancel: () => void handleConfirmRevert(false) }) })] }));
|
|
478
|
+
if (mode === "reverting")
|
|
479
|
+
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: _jsx(Spinner, { label: `Reverting "${rows[cursor]?.name}" and restarting...` }) }));
|
|
400
480
|
return null;
|
|
401
481
|
};
|
|
402
482
|
// ── Coworker table ────────────────────────────────────────────────────────
|
|
403
483
|
const actionPanel = renderActionPanel();
|
|
404
484
|
const panelHeight = actionPanel ? 5 : 0;
|
|
405
485
|
const tableHeight = contentHeight - panelHeight - 3;
|
|
406
|
-
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: "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) => {
|
|
486
|
+
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) => {
|
|
407
487
|
const selected = i === cursor;
|
|
408
488
|
const revealed = revealedRows.has(s.id);
|
|
409
|
-
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 ? "magenta" : undefined, dimColor: !selected && !s.mode, children: (s.mode ?? "—").padEnd(12) }), _jsx(Text, { dimColor: !selected, children: s.session_id.padEnd(36) }), revealed
|
|
489
|
+
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.mode, children: (s.mode ?? "—").padEnd(12) }), _jsx(Text, { dimColor: !selected, children: s.session_id.padEnd(36) }), revealed
|
|
410
490
|
? _jsx(Text, { color: "yellow", children: s.agent_code })
|
|
411
491
|
: _jsx(Text, { dimColor: true, children: MASKED_CODE })] }, s.id));
|
|
412
492
|
})] }))] }));
|
|
@@ -6,11 +6,17 @@ export function SessionSidebar({ serverUrl, password }) {
|
|
|
6
6
|
const { listSessions } = useApi(serverUrl, password);
|
|
7
7
|
const [sessions, setSessions] = useState([]);
|
|
8
8
|
useEffect(() => {
|
|
9
|
+
// Initial fetch
|
|
9
10
|
listSessions().then((s) => setSessions(s));
|
|
11
|
+
// Poll every 5 seconds to refresh statuses
|
|
12
|
+
const interval = setInterval(() => {
|
|
13
|
+
listSessions().then((s) => setSessions(s));
|
|
14
|
+
}, 5000);
|
|
15
|
+
return () => clearInterval(interval);
|
|
10
16
|
}, [listSessions]);
|
|
11
17
|
if (sessions.length === 0)
|
|
12
18
|
return null;
|
|
13
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, color: "cyan", children: "Sessions" }), sessions.map((session, i) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: getNodeColor(i), children: "\u25CF" }), _jsx(Text, { children: session.name })] }, session.id)))] }));
|
|
19
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, color: "cyan", children: "Sessions" }), sessions.map((session, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, flexDirection: "row", children: [_jsx(Text, { color: getNodeColor(i), children: "\u25CF" }), _jsx(Text, { children: session.name })] }), session.status && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: session.status }) }))] }, session.id)))] }));
|
|
14
20
|
}
|
|
15
21
|
function getNodeColor(index) {
|
|
16
22
|
const colors = ["green", "blue", "yellow", "magenta", "cyan", "red", "white"];
|
|
@@ -61,12 +61,61 @@ export function TailMessages({ serverUrl, password, onBack, contentHeight }) {
|
|
|
61
61
|
const messageLines = [];
|
|
62
62
|
for (const msg of messages) {
|
|
63
63
|
for (const part of msg.parts) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
if (part.type === "text" && part.text) {
|
|
65
|
+
// Split multiline text into individual lines for rendering
|
|
66
|
+
const lines = part.text.split("\n");
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
messageLines.push({ role: msg.role, text: i === 0 ? lines[i] : ` ${lines[i]}` });
|
|
69
|
+
}
|
|
70
|
+
messageLines.push({ role: msg.role, text: "" }); // blank line between parts
|
|
71
|
+
}
|
|
72
|
+
else if (part.type === "tool") {
|
|
73
|
+
// Tool part has: tool (string), input, output
|
|
74
|
+
const toolName = typeof part.tool === "string" ? part.tool : String(part.tool ?? "unknown");
|
|
75
|
+
messageLines.push({ role: msg.role, text: "" }); // Blank line before tool
|
|
76
|
+
messageLines.push({ role: msg.role, text: `▶ Tool: ${toolName}` });
|
|
77
|
+
if (part.input !== undefined) {
|
|
78
|
+
// Try to pretty-print input if it's an object
|
|
79
|
+
let inputStr;
|
|
80
|
+
if (typeof part.input === "object" && part.input !== null) {
|
|
81
|
+
inputStr = JSON.stringify(part.input, null, 2);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
inputStr = String(part.input);
|
|
85
|
+
}
|
|
86
|
+
const lines = inputStr.split("\n");
|
|
87
|
+
for (let i = 0; i < Math.min(lines.length, 15); i++) {
|
|
88
|
+
messageLines.push({ role: msg.role, text: ` ${lines[i]}` });
|
|
89
|
+
}
|
|
90
|
+
if (lines.length > 15) {
|
|
91
|
+
messageLines.push({ role: msg.role, text: ` [...] (${lines.length - 15} more lines)` });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (part.output !== undefined) {
|
|
95
|
+
// Show output type and preview
|
|
96
|
+
let outputPreview;
|
|
97
|
+
if (typeof part.output === "object" && part.output !== null) {
|
|
98
|
+
outputPreview = JSON.stringify(part.output, null, 2).slice(0, 200);
|
|
99
|
+
}
|
|
100
|
+
else if (typeof part.output === "string") {
|
|
101
|
+
outputPreview = part.output.slice(0, 200);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
outputPreview = String(part.output).slice(0, 200);
|
|
105
|
+
}
|
|
106
|
+
const outputType = typeof part.output === "object" ? "object" : typeof part.output;
|
|
107
|
+
messageLines.push({ role: msg.role, text: ` ${"—".repeat(40)}` });
|
|
108
|
+
messageLines.push({ role: msg.role, text: ` Result (${outputType}):` });
|
|
109
|
+
const lines = outputPreview.split("\n");
|
|
110
|
+
for (let i = 0; i < Math.min(lines.length, 5); i++) {
|
|
111
|
+
messageLines.push({ role: msg.role, text: ` ${lines[i]}` });
|
|
112
|
+
}
|
|
113
|
+
if (lines.length > 5 || outputPreview.length >= 200) {
|
|
114
|
+
messageLines.push({ role: msg.role, text: " [...]" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
messageLines.push({ role: msg.role, text: "" }); // Blank line after tool
|
|
68
118
|
}
|
|
69
|
-
messageLines.push({ role: msg.role, text: "" }); // blank line between parts
|
|
70
119
|
}
|
|
71
120
|
}
|
|
72
121
|
const viewHeight = contentHeight - 3; // header + separator
|
|
@@ -4,6 +4,7 @@ export interface Session {
|
|
|
4
4
|
session_id: string;
|
|
5
5
|
agent_code: string;
|
|
6
6
|
mode: string | null;
|
|
7
|
+
status: string | null;
|
|
7
8
|
created_at: string;
|
|
8
9
|
}
|
|
9
10
|
export interface AppMode {
|
|
@@ -12,8 +13,12 @@ export interface AppMode {
|
|
|
12
13
|
model: string;
|
|
13
14
|
}
|
|
14
15
|
export interface MessagePart {
|
|
15
|
-
type: "text";
|
|
16
|
-
text
|
|
16
|
+
type: "text" | "tool" | "tool-output";
|
|
17
|
+
text?: string;
|
|
18
|
+
tool?: string;
|
|
19
|
+
input?: unknown;
|
|
20
|
+
output?: unknown;
|
|
21
|
+
data?: unknown;
|
|
17
22
|
}
|
|
18
23
|
export interface SessionMessage {
|
|
19
24
|
role: "user" | "assistant";
|
|
@@ -60,6 +65,10 @@ export declare function useApi(serverUrl: string, password: string): {
|
|
|
60
65
|
ok: boolean;
|
|
61
66
|
messageID: string;
|
|
62
67
|
}>;
|
|
68
|
+
revertToStart: (name: string) => Promise<{
|
|
69
|
+
ok: boolean;
|
|
70
|
+
messageID: string;
|
|
71
|
+
}>;
|
|
63
72
|
regenerateCode: (name: string) => Promise<Session>;
|
|
64
73
|
getConfig: () => Promise<Config>;
|
|
65
74
|
setConfig: (key: string, value: string) => Promise<{
|
|
@@ -57,6 +57,9 @@ export function useApi(serverUrl, password) {
|
|
|
57
57
|
const injectText = useCallback(async (name, text) => {
|
|
58
58
|
return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/inject`, password, { method: "POST", body: JSON.stringify({ text }) });
|
|
59
59
|
}, [base, password]);
|
|
60
|
+
const revertToStart = useCallback(async (name) => {
|
|
61
|
+
return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/revert-to-start`, password, { method: "POST" });
|
|
62
|
+
}, [base, password]);
|
|
60
63
|
const regenerateCode = useCallback(async (name) => {
|
|
61
64
|
return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/regenerate-code`, password, { method: "POST" });
|
|
62
65
|
}, [base, password]);
|
|
@@ -110,6 +113,7 @@ export function useApi(serverUrl, password) {
|
|
|
110
113
|
checkHealth,
|
|
111
114
|
getMessages,
|
|
112
115
|
injectText,
|
|
116
|
+
revertToStart,
|
|
113
117
|
regenerateCode,
|
|
114
118
|
getConfig,
|
|
115
119
|
setConfig,
|
package/dist/server/routes.js
CHANGED
|
@@ -4,12 +4,111 @@ const MAIL_INJECTION_BLURB = [
|
|
|
4
4
|
``,
|
|
5
5
|
`---`,
|
|
6
6
|
`You have a new message. Please review the injected message above and respond accordingly.`,
|
|
7
|
-
`
|
|
8
|
-
`
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
`
|
|
7
|
+
`IMPORTANT: When reading or responding, note that dollar signs ($) and other special`,
|
|
8
|
+
`characters may be interpreted as markdown. The sender may have included them but they`,
|
|
9
|
+
`could appear differently in your session view. Interpret context accordingly.`,
|
|
10
|
+
``,
|
|
11
|
+
`When responding to the sender:`,
|
|
12
|
+
`- Use the \`agent-office worker send-message\` tool so they can see your reply`,
|
|
13
|
+
`- Avoid excessive length - keep responses concise`,
|
|
14
|
+
`- Use markdown front-matter with a "choices" array if offering options`,
|
|
15
|
+
``,
|
|
16
|
+
`Tip: For currency or prices, use code blocks. Example: put numbers in single or`,
|
|
17
|
+
`double quotes to preserve formatting characters like dollar signs.`,
|
|
12
18
|
].join("\n");
|
|
19
|
+
function generateWelcomeMessage(name, mode, status, humanName, humanDescription, token) {
|
|
20
|
+
return [
|
|
21
|
+
`╔══════════════════════════════════════════════════════╗`,
|
|
22
|
+
`║ WELCOME TO THE AGENT OFFICE ║`,
|
|
23
|
+
`╚══════════════════════════════════════════════════════╝`,
|
|
24
|
+
``,
|
|
25
|
+
`You are now clocked in.`,
|
|
26
|
+
` Name: ${name}`,
|
|
27
|
+
...(mode ? [` Mode: ${mode}`] : []),
|
|
28
|
+
...(status ? [` Status: ${status}`] : []),
|
|
29
|
+
` Human manager: ${humanName} — the human who created your`,
|
|
30
|
+
` session, assigns your work, and is your`,
|
|
31
|
+
` primary point of contact for questions,`,
|
|
32
|
+
` updates, and decisions.`,
|
|
33
|
+
...(humanDescription ? [
|
|
34
|
+
` "${humanDescription}"`,
|
|
35
|
+
] : []),
|
|
36
|
+
``,
|
|
37
|
+
`The agent-office CLI is your PRIMARY means of communicating`,
|
|
38
|
+
`with your human manager (${humanName}) and your coworkers.`,
|
|
39
|
+
`Use it to send and receive messages, and to discover who`,
|
|
40
|
+
`else is working.`,
|
|
41
|
+
``,
|
|
42
|
+
`════════════════════════════════════════════════════════`,
|
|
43
|
+
` AVAILABLE COMMANDS`,
|
|
44
|
+
`════════════════════════════════════════════════════════`,
|
|
45
|
+
``,
|
|
46
|
+
` List your coworkers`,
|
|
47
|
+
` agent-office worker list-coworkers \\`,
|
|
48
|
+
` ${token}`,
|
|
49
|
+
``,
|
|
50
|
+
` Set your public status (visible to coworkers and manager)`,
|
|
51
|
+
` agent-office worker set-status \\`,
|
|
52
|
+
` --status "your status here" \\`,
|
|
53
|
+
` ${token}`,
|
|
54
|
+
``,
|
|
55
|
+
` Clear your public status`,
|
|
56
|
+
` agent-office worker set-status \\`,
|
|
57
|
+
` --clear \\`,
|
|
58
|
+
` ${token}`,
|
|
59
|
+
``,
|
|
60
|
+
` Send a message to your manager or a coworker`,
|
|
61
|
+
` agent-office worker send-message \\`,
|
|
62
|
+
` --name <recipient-name> \\`,
|
|
63
|
+
` --body "Your message here" \\`,
|
|
64
|
+
` ${token}`,
|
|
65
|
+
``,
|
|
66
|
+
` Send a message to multiple recipients at once`,
|
|
67
|
+
` agent-office worker send-message \\`,
|
|
68
|
+
` --name alice --name bob \\`,
|
|
69
|
+
` --body "Your message here" \\`,
|
|
70
|
+
` ${token}`,
|
|
71
|
+
``,
|
|
72
|
+
` Manage scheduled tasks (optional)`,
|
|
73
|
+
` agent-office worker cron \\`,
|
|
74
|
+
` ${token}`,
|
|
75
|
+
``,
|
|
76
|
+
`════════════════════════════════════════════════════════`,
|
|
77
|
+
` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
|
|
78
|
+
`════════════════════════════════════════════════════════`,
|
|
79
|
+
``,
|
|
80
|
+
` Nobody — not ${humanName}, not your coworkers — can see`,
|
|
81
|
+
` anything you think, reason, or write inside this session.`,
|
|
82
|
+
` Your work is completely private until you explicitly send`,
|
|
83
|
+
` a message using the send-message command.`,
|
|
84
|
+
``,
|
|
85
|
+
` This means:`,
|
|
86
|
+
` • You must send a message to report progress or ask`,
|
|
87
|
+
` for help. Silence looks like inaction.`,
|
|
88
|
+
` • Finishing a task without messaging ${humanName}`,
|
|
89
|
+
` means they will never know it is done.`,
|
|
90
|
+
` • If you are blocked or uncertain, send a message —`,
|
|
91
|
+
` nobody will know otherwise.`,
|
|
92
|
+
``,
|
|
93
|
+
`════════════════════════════════════════════════════════`,
|
|
94
|
+
` TIPS`,
|
|
95
|
+
`════════════════════════════════════════════════════════`,
|
|
96
|
+
``,
|
|
97
|
+
` - Run list-coworkers to discover who is available and`,
|
|
98
|
+
` what their names are before sending messages.`,
|
|
99
|
+
` - Messages you send are delivered directly into the`,
|
|
100
|
+
` recipient's active session — they will see them`,
|
|
101
|
+
` immediately.`,
|
|
102
|
+
` - Your human manager is ${humanName}. They can send you`,
|
|
103
|
+
` messages at any time and those will appear here in`,
|
|
104
|
+
` your session just like this one. You can reach them`,
|
|
105
|
+
` by sending a message to --name ${humanName}.`,
|
|
106
|
+
` - Optional: Set up recurring scheduled tasks with cron`,
|
|
107
|
+
` jobs. Run 'agent-office worker cron list ${token}' to`,
|
|
108
|
+
` get started.`,
|
|
109
|
+
``,
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
13
112
|
export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
14
113
|
const router = Router();
|
|
15
114
|
router.get("/health", (_req, res) => {
|
|
@@ -36,7 +135,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
|
36
135
|
router.get("/sessions", async (_req, res) => {
|
|
37
136
|
try {
|
|
38
137
|
const rows = await sql `
|
|
39
|
-
SELECT id, name, session_id, agent_code, mode, created_at
|
|
138
|
+
SELECT id, name, session_id, agent_code, mode, status, created_at
|
|
40
139
|
FROM sessions
|
|
41
140
|
ORDER BY created_at DESC
|
|
42
141
|
`;
|
|
@@ -95,12 +194,11 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
|
95
194
|
const defaultEntry = Object.entries(providers.default)[0];
|
|
96
195
|
if (defaultEntry) {
|
|
97
196
|
const clockInToken = `${row.agent_code}@${serverUrl}`;
|
|
98
|
-
const
|
|
99
|
-
const firstMessage = `You have been enrolled in the agent office.${modeNote}\n\nTo clock in and receive your full briefing, run:\n\n agent-office worker clock-in ${clockInToken}`;
|
|
197
|
+
const enrollmentMessage = `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 ${clockInToken}`;
|
|
100
198
|
await opencode.session.chat(opencodeSessionId, {
|
|
101
199
|
modelID: defaultEntry[0],
|
|
102
200
|
providerID: defaultEntry[1],
|
|
103
|
-
parts: [{ type: "text", text:
|
|
201
|
+
parts: [{ type: "text", text: enrollmentMessage }],
|
|
104
202
|
...(trimmedMode ? { mode: trimmedMode } : {}),
|
|
105
203
|
});
|
|
106
204
|
}
|
|
@@ -150,9 +248,26 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
|
150
248
|
.slice(-limit)
|
|
151
249
|
.map((m) => ({
|
|
152
250
|
role: m.info.role,
|
|
153
|
-
parts: m.parts
|
|
154
|
-
|
|
155
|
-
|
|
251
|
+
parts: m.parts.map((p) => {
|
|
252
|
+
const part = p;
|
|
253
|
+
if (part.type === "text") {
|
|
254
|
+
return { type: "text", text: part.text ?? "" };
|
|
255
|
+
}
|
|
256
|
+
else if (part.type === "tool") {
|
|
257
|
+
// Extract tool name, input, and output from the tool state
|
|
258
|
+
const state = part.state;
|
|
259
|
+
return {
|
|
260
|
+
type: "tool",
|
|
261
|
+
tool: part.tool,
|
|
262
|
+
input: state?.input,
|
|
263
|
+
output: state?.output,
|
|
264
|
+
data: part,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
return { type: part.type, data: part };
|
|
269
|
+
}
|
|
270
|
+
}),
|
|
156
271
|
}))
|
|
157
272
|
.filter((m) => m.parts.length > 0);
|
|
158
273
|
res.json(result);
|
|
@@ -209,6 +324,48 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
|
209
324
|
res.status(502).json({ error: "Failed to inject message into OpenCode session", detail: String(err) });
|
|
210
325
|
}
|
|
211
326
|
});
|
|
327
|
+
router.post("/sessions/:name/revert-to-start", async (req, res) => {
|
|
328
|
+
const { name } = req.params;
|
|
329
|
+
const rows = await sql `
|
|
330
|
+
SELECT id, name, session_id, agent_code, mode, status, created_at FROM sessions WHERE name = ${name}
|
|
331
|
+
`;
|
|
332
|
+
if (rows.length === 0) {
|
|
333
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const session = rows[0];
|
|
337
|
+
try {
|
|
338
|
+
const messages = await opencode.session.messages(session.session_id);
|
|
339
|
+
if (messages.length === 0) {
|
|
340
|
+
res.status(400).json({ error: "Session has no messages to revert to" });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const firstMessage = messages[0];
|
|
344
|
+
if (!firstMessage || !firstMessage.info || !firstMessage.info.id) {
|
|
345
|
+
res.status(500).json({ error: "Failed to get first message ID" });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
await opencode.session.revert(session.session_id, { messageID: firstMessage.info.id });
|
|
349
|
+
const providers = await opencode.app.providers();
|
|
350
|
+
const defaultEntry = Object.entries(providers.default)[0];
|
|
351
|
+
if (!defaultEntry) {
|
|
352
|
+
res.status(502).json({ error: "No default model configured in OpenCode" });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const clockInToken = `${session.agent_code}@${serverUrl}`;
|
|
356
|
+
const enrollmentMessage = `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 ${clockInToken}`;
|
|
357
|
+
await opencode.session.chat(session.session_id, {
|
|
358
|
+
modelID: defaultEntry[0],
|
|
359
|
+
providerID: defaultEntry[1],
|
|
360
|
+
parts: [{ type: "text", text: enrollmentMessage }],
|
|
361
|
+
});
|
|
362
|
+
res.json({ ok: true, messageID: firstMessage.info.id });
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error("POST /sessions/:name/revert-to-start error:", err);
|
|
366
|
+
res.status(502).json({ error: "Failed to revert session", detail: String(err) });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
212
369
|
router.delete("/sessions/:name", async (req, res) => {
|
|
213
370
|
const { name } = req.params;
|
|
214
371
|
const rows = await sql `
|
|
@@ -784,6 +941,42 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
|
784
941
|
res.status(500).json({ error: "Internal server error" });
|
|
785
942
|
}
|
|
786
943
|
});
|
|
944
|
+
router.post("/worker/set-status", async (req, res) => {
|
|
945
|
+
const { code } = req.query;
|
|
946
|
+
const { status } = req.body;
|
|
947
|
+
if (!code || typeof code !== "string") {
|
|
948
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const trimmedStatus = status === null ? null : (status === undefined ? null : status.trim());
|
|
952
|
+
if (trimmedStatus !== null && trimmedStatus.length > 140) {
|
|
953
|
+
res.status(400).json({ error: "status must be at most 140 characters" });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const rows = await sql `
|
|
957
|
+
SELECT id, name, session_id, agent_code, mode, status, created_at
|
|
958
|
+
FROM sessions
|
|
959
|
+
WHERE agent_code = ${code}
|
|
960
|
+
`;
|
|
961
|
+
if (rows.length === 0) {
|
|
962
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const session = rows[0];
|
|
966
|
+
try {
|
|
967
|
+
await sql `
|
|
968
|
+
UPDATE sessions
|
|
969
|
+
SET status = ${trimmedStatus}
|
|
970
|
+
WHERE agent_code = ${code}
|
|
971
|
+
`;
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
console.error("POST /worker/set-status error:", err);
|
|
975
|
+
res.status(500).json({ error: "Internal server error" });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
res.json({ ok: true, name: session.name, status: trimmedStatus });
|
|
979
|
+
});
|
|
787
980
|
router.post("/worker/send-message", async (req, res) => {
|
|
788
981
|
const { code } = req.query;
|
|
789
982
|
const { to, body } = req.body;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-office",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Manage OpenCode sessions with named aliases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@inkjs/ui": "^2.0.0",
|
|
35
35
|
"@opencode-ai/sdk": "^0.1.0-alpha.21",
|
|
36
|
+
"agent-office": "^0.0.2",
|
|
36
37
|
"commander": "^12.0.0",
|
|
37
38
|
"croner": "^10.0.1",
|
|
38
39
|
"dotenv": "^16.0.0",
|