agent-office 0.0.1 → 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.
@@ -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
@@ -3,11 +3,22 @@ export interface Session {
3
3
  name: string;
4
4
  session_id: string;
5
5
  agent_code: string;
6
+ mode: string | null;
7
+ status: string | null;
6
8
  created_at: string;
7
9
  }
10
+ export interface AppMode {
11
+ name: string;
12
+ description: string;
13
+ model: string;
14
+ }
8
15
  export interface MessagePart {
9
- type: "text";
10
- text: string;
16
+ type: "text" | "tool" | "tool-output";
17
+ text?: string;
18
+ tool?: string;
19
+ input?: unknown;
20
+ output?: unknown;
21
+ data?: unknown;
11
22
  }
12
23
  export interface SessionMessage {
13
24
  role: "user" | "assistant";
@@ -25,9 +36,28 @@ export interface MailMessage {
25
36
  export interface Config {
26
37
  [key: string]: string;
27
38
  }
39
+ export interface CronJob {
40
+ id: number;
41
+ name: string;
42
+ session_name: string;
43
+ schedule: string;
44
+ timezone: string | null;
45
+ message: string;
46
+ enabled: boolean;
47
+ created_at: string;
48
+ last_run: string | null;
49
+ next_run: string | null;
50
+ }
51
+ export interface CronHistoryEntry {
52
+ id: number;
53
+ cron_job_id: number;
54
+ executed_at: string;
55
+ success: boolean;
56
+ error_message: string | null;
57
+ }
28
58
  export declare function useApi(serverUrl: string, password: string): {
29
59
  listSessions: () => Promise<Session[]>;
30
- createSession: (name: string) => Promise<Session>;
60
+ createSession: (name: string, mode?: string) => Promise<Session>;
31
61
  deleteSession: (name: string) => Promise<void>;
32
62
  checkHealth: () => Promise<boolean>;
33
63
  getMessages: (name: string, limit?: number) => Promise<SessionMessage[]>;
@@ -35,6 +65,10 @@ export declare function useApi(serverUrl: string, password: string): {
35
65
  ok: boolean;
36
66
  messageID: string;
37
67
  }>;
68
+ revertToStart: (name: string) => Promise<{
69
+ ok: boolean;
70
+ messageID: string;
71
+ }>;
38
72
  regenerateCode: (name: string) => Promise<Session>;
39
73
  getConfig: () => Promise<Config>;
40
74
  setConfig: (key: string, value: string) => Promise<{
@@ -55,6 +89,23 @@ export declare function useApi(serverUrl: string, password: string): {
55
89
  }>;
56
90
  }>;
57
91
  markMessageRead: (id: number) => Promise<MailMessage>;
92
+ getModes: () => Promise<AppMode[]>;
93
+ listCrons: (sessionName?: string) => Promise<CronJob[]>;
94
+ createCron: (data: {
95
+ name: string;
96
+ session_name: string;
97
+ schedule: string;
98
+ message: string;
99
+ timezone?: string;
100
+ }) => Promise<CronJob>;
101
+ deleteCron: (id: number) => Promise<{
102
+ deleted: boolean;
103
+ id: number;
104
+ name: string;
105
+ }>;
106
+ enableCron: (id: number) => Promise<CronJob>;
107
+ disableCron: (id: number) => Promise<CronJob>;
108
+ getCronHistory: (id: number, limit?: number) => Promise<CronHistoryEntry[]>;
58
109
  };
59
110
  export declare function useAsyncState<T>(): {
60
111
  run: (fn: () => Promise<T>) => Promise<T | null>;
@@ -28,12 +28,15 @@ 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, mode) => {
32
32
  return apiFetch(`${base}/sessions`, password, {
33
33
  method: "POST",
34
- body: JSON.stringify({ name }),
34
+ body: JSON.stringify({ name, ...(mode ? { mode } : {}) }),
35
35
  });
36
36
  }, [base, password]);
37
+ const getModes = useCallback(async () => {
38
+ return apiFetch(`${base}/modes`, password);
39
+ }, [base, password]);
37
40
  const deleteSession = useCallback(async (name) => {
38
41
  await apiFetch(`${base}/sessions/${encodeURIComponent(name)}`, password, {
39
42
  method: "DELETE",
@@ -54,6 +57,9 @@ export function useApi(serverUrl, password) {
54
57
  const injectText = useCallback(async (name, text) => {
55
58
  return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/inject`, password, { method: "POST", body: JSON.stringify({ text }) });
56
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]);
57
63
  const regenerateCode = useCallback(async (name) => {
58
64
  return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/regenerate-code`, password, { method: "POST" });
59
65
  }, [base, password]);
@@ -78,6 +84,28 @@ export function useApi(serverUrl, password) {
78
84
  const markMessageRead = useCallback(async (id) => {
79
85
  return apiFetch(`${base}/messages/${id}/read`, password, { method: "POST" });
80
86
  }, [base, password]);
87
+ const listCrons = useCallback(async (sessionName) => {
88
+ const params = sessionName ? `?session_name=${encodeURIComponent(sessionName)}` : "";
89
+ return apiFetch(`${base}/crons${params}`, password);
90
+ }, [base, password]);
91
+ const createCron = useCallback(async (data) => {
92
+ return apiFetch(`${base}/crons`, password, {
93
+ method: "POST",
94
+ body: JSON.stringify(data),
95
+ });
96
+ }, [base, password]);
97
+ const deleteCron = useCallback(async (id) => {
98
+ return apiFetch(`${base}/crons/${id}`, password, { method: "DELETE" });
99
+ }, [base, password]);
100
+ const enableCron = useCallback(async (id) => {
101
+ return apiFetch(`${base}/crons/${id}/enable`, password, { method: "POST" });
102
+ }, [base, password]);
103
+ const disableCron = useCallback(async (id) => {
104
+ return apiFetch(`${base}/crons/${id}/disable`, password, { method: "POST" });
105
+ }, [base, password]);
106
+ const getCronHistory = useCallback(async (id, limit = 10) => {
107
+ return apiFetch(`${base}/crons/${id}/history?limit=${limit}`, password);
108
+ }, [base, password]);
81
109
  return {
82
110
  listSessions,
83
111
  createSession,
@@ -85,12 +113,20 @@ export function useApi(serverUrl, password) {
85
113
  checkHealth,
86
114
  getMessages,
87
115
  injectText,
116
+ revertToStart,
88
117
  regenerateCode,
89
118
  getConfig,
90
119
  setConfig,
91
120
  getMailMessages,
92
121
  sendMailMessage,
93
122
  markMessageRead,
123
+ getModes,
124
+ listCrons,
125
+ createCron,
126
+ deleteCron,
127
+ enableCron,
128
+ disableCron,
129
+ getCronHistory,
94
130
  };
95
131
  }
96
132
  export function useAsyncState() {
@@ -0,0 +1,24 @@
1
+ import type { Sql } from "../db/index.js";
2
+ import type { OpencodeClient } from "../lib/opencode.js";
3
+ import type { CronJobRow } from "../db/index.js";
4
+ interface CronSchedulerOptions {
5
+ onJobExecuted?: (jobId: number, success: boolean, error?: string) => void;
6
+ }
7
+ export declare class CronScheduler {
8
+ private options;
9
+ private activeJobs;
10
+ private sql;
11
+ private opencode;
12
+ private started;
13
+ constructor(options?: CronSchedulerOptions);
14
+ start(sql: Sql, opencode: OpencodeClient): Promise<void>;
15
+ stop(): void;
16
+ private addJob;
17
+ private executeJob;
18
+ addCronJob(job: CronJobRow): void;
19
+ removeCronJob(jobId: number): void;
20
+ enableCronJob(job: CronJobRow): void;
21
+ disableCronJob(jobId: number): void;
22
+ isTracking(jobId: number): boolean;
23
+ }
24
+ export {};
@@ -0,0 +1,121 @@
1
+ import { Cron } from "croner";
2
+ const CRON_INJECTION_BLURB = [
3
+ ``,
4
+ `---`,
5
+ `You have a scheduled cron job trigger. Please review the injected message above`,
6
+ `and take appropriate action.`,
7
+ ].join("\n");
8
+ export class CronScheduler {
9
+ options;
10
+ activeJobs = new Map();
11
+ sql = null;
12
+ opencode = null;
13
+ started = false;
14
+ constructor(options = {}) {
15
+ this.options = options;
16
+ }
17
+ async start(sql, opencode) {
18
+ if (this.started)
19
+ return;
20
+ this.sql = sql;
21
+ this.opencode = opencode;
22
+ const rows = await sql `
23
+ SELECT id, name, schedule, timezone, message, session_name
24
+ FROM cron_jobs
25
+ WHERE enabled = TRUE
26
+ `;
27
+ for (const job of rows) {
28
+ this.addJob(job);
29
+ }
30
+ this.started = true;
31
+ console.log(`Cron scheduler started: tracking ${rows.length} active jobs`);
32
+ }
33
+ stop() {
34
+ for (const cronInst of this.activeJobs.values()) {
35
+ cronInst.cron.stop();
36
+ }
37
+ this.activeJobs.clear();
38
+ this.started = false;
39
+ console.log("Cron scheduler stopped");
40
+ }
41
+ addJob(job) {
42
+ if (!this.opencode || !this.sql)
43
+ return;
44
+ const options = {
45
+ protect: true,
46
+ name: job.name,
47
+ };
48
+ if (job.timezone) {
49
+ options.timezone = job.timezone;
50
+ }
51
+ const cron = new Cron(job.schedule, options, async () => {
52
+ await this.executeJob(job);
53
+ });
54
+ this.activeJobs.set(job.id, { cron, job });
55
+ }
56
+ async executeJob(job) {
57
+ if (!this.sql || !this.opencode)
58
+ return;
59
+ const executedAt = new Date();
60
+ try {
61
+ const [session] = await this.sql `
62
+ SELECT session_id FROM sessions WHERE name = ${job.session_name}
63
+ `;
64
+ if (!session) {
65
+ throw new Error(`Session "${job.session_name}" not found`);
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
+ const injectText = `[Cron Job "${job.name}" — ${executedAt.toISOString()}]\n${job.message}${CRON_INJECTION_BLURB}`;
73
+ await this.opencode.session.chat(session.session_id, {
74
+ modelID: defaultEntry[0],
75
+ providerID: defaultEntry[1],
76
+ parts: [{ type: "text", text: injectText }],
77
+ });
78
+ await this.sql `
79
+ UPDATE cron_jobs SET last_run = ${executedAt} WHERE id = ${job.id}
80
+ `;
81
+ await this.sql `
82
+ INSERT INTO cron_history (cron_job_id, executed_at, success)
83
+ VALUES (${job.id}, ${executedAt}, TRUE)
84
+ `;
85
+ this.options.onJobExecuted?.(job.id, true);
86
+ console.log(`Cron job "${job.name}" executed successfully`);
87
+ }
88
+ catch (err) {
89
+ const errorMessage = err instanceof Error ? err.message : String(err);
90
+ await this.sql `
91
+ INSERT INTO cron_history (cron_job_id, executed_at, success, error_message)
92
+ VALUES (${job.id}, ${executedAt}, FALSE, ${errorMessage})
93
+ `;
94
+ this.options.onJobExecuted?.(job.id, false, errorMessage);
95
+ console.error(`Cron job "${job.name}" failed:`, errorMessage);
96
+ }
97
+ }
98
+ addCronJob(job) {
99
+ if (job.enabled) {
100
+ this.addJob(job);
101
+ }
102
+ }
103
+ removeCronJob(jobId) {
104
+ const cronInst = this.activeJobs.get(jobId);
105
+ if (cronInst) {
106
+ cronInst.cron.stop();
107
+ this.activeJobs.delete(jobId);
108
+ }
109
+ }
110
+ enableCronJob(job) {
111
+ if (!this.activeJobs.has(job.id) && job.enabled) {
112
+ this.addJob(job);
113
+ }
114
+ }
115
+ disableCronJob(jobId) {
116
+ this.removeCronJob(jobId);
117
+ }
118
+ isTracking(jobId) {
119
+ return this.activeJobs.has(jobId);
120
+ }
121
+ }
@@ -1,3 +1,4 @@
1
1
  import type { Sql } from "../db/index.js";
2
2
  import type { OpencodeClient } from "../lib/opencode.js";
3
- export declare function createApp(sql: Sql, opencode: OpencodeClient, password: string, serverUrl: string): import("express-serve-static-core").Express;
3
+ import { CronScheduler } from "./cron.js";
4
+ export declare function createApp(sql: Sql, opencode: OpencodeClient, password: string, serverUrl: string, cronScheduler: CronScheduler): import("express-serve-static-core").Express;
@@ -10,13 +10,13 @@ function authMiddleware(password) {
10
10
  next();
11
11
  };
12
12
  }
13
- export function createApp(sql, opencode, password, serverUrl) {
13
+ export function createApp(sql, opencode, password, serverUrl, cronScheduler) {
14
14
  const app = express();
15
15
  app.use(express.json());
16
16
  // Worker routes are unauthenticated — mounted before auth middleware
17
17
  app.use("/", createWorkerRouter(sql, opencode, serverUrl));
18
18
  // Everything else requires Bearer auth
19
19
  app.use(authMiddleware(password));
20
- app.use("/", createRouter(sql, opencode, serverUrl));
20
+ app.use("/", createRouter(sql, opencode, serverUrl, cronScheduler));
21
21
  return app;
22
22
  }
@@ -1,5 +1,6 @@
1
1
  import { Router } from "express";
2
2
  import type { Sql } from "../db/index.js";
3
3
  import type { OpencodeClient } from "../lib/opencode.js";
4
- export declare function createRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string): Router;
4
+ import { CronScheduler } from "./cron.js";
5
+ export declare function createRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string, scheduler: CronScheduler): Router;
5
6
  export declare function createWorkerRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string): Router;