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 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: {
@@ -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));
@@ -7,6 +7,7 @@ export interface SessionRow {
7
7
  session_id: string;
8
8
  agent_code: string;
9
9
  mode: string | null;
10
+ status: string | null;
10
11
  created_at: Date;
11
12
  }
12
13
  export interface ConfigRow {
@@ -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 `
@@ -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
- const lines = part.text.split("\n");
38
- for (let i = 0; i < lines.length; i++) {
39
- messageLines.push({ role: msg.role, text: i === 0 ? lines[i] : ` ${lines[i]}` });
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
- // 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]}` });
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: string;
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,
@@ -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
- `Respond using markdown. Your markdown front-matter can contain a property "choices" which`,
8
- `is an array of choices for the message sender to choose from. These choices are optional`,
9
- `and shouldn't alter your authentic personality in your responses.`,
10
- `IMPORTANT: in order for the sender to see your response, you must send them a message back`,
11
- `using the \`agent-office worker send-message\` tool. Also, don't go on for too long if things seem repetitive.`,
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 modeNote = trimmedMode ? `\n Mode: ${trimmedMode}` : "";
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: firstMessage }],
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
- .filter((p) => p.type === "text")
155
- .map((p) => ({ type: "text", text: p.text })),
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.2",
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",