agent-office 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -207,6 +207,12 @@ agent-office worker set-status --clear <token>
207
207
  # Send a message to one or more recipients
208
208
  agent-office worker send-message --name Alice --name Bob --body "Status update: PR is ready" <token>
209
209
 
210
+ # If your message body contains $ characters, escape them to avoid shell variable expansion
211
+ agent-office worker send-message --name Alice --body "Cost is \$50" <token>
212
+
213
+ # > Warning: always escape $ in --body when using double quotes in bash, or use single quotes.
214
+ # > Unescaped $ will be silently expanded by the shell before the message is sent.
215
+
210
216
  # Cron job management
211
217
  agent-office worker cron list <token>
212
218
  agent-office worker cron create --name "daily-report" --schedule "0 9 * * *" --message "Send daily status" <token>
package/dist/cli.js CHANGED
@@ -102,6 +102,23 @@ workerCmd
102
102
  .argument("<token>", "Agent token in the format <agent_code>@<server-url>")
103
103
  .requiredOption("--name <name>", "Recipient name (can be specified multiple times)", (val, prev) => [...prev, val], [])
104
104
  .requiredOption("--body <body>", "Message body")
105
+ .addHelpText("after", `
106
+ Examples:
107
+ agent-office worker send-message --name Alice --body "PR is ready" <token>
108
+ agent-office worker send-message --name Alice --name Bob --body "Hello" <token>
109
+
110
+ # Message body containing $: escape with \\ or use single quotes
111
+ agent-office worker send-message --name Alice --body "Cost is \\$50" <token>
112
+ agent-office worker send-message --name Alice --body 'Cost is $50' <token>
113
+
114
+ Warning:
115
+ The shell processes --body before the CLI receives it. Be mindful of bash
116
+ special characters in your message body:
117
+ $var, \${x} variable expansion → escape as \\$ or use single quotes
118
+ \`cmd\`, $(x) command substitution → escape backticks or use single quotes
119
+ !, \\\\, " history/escape chars → escape or use single quotes
120
+ The safest option is always single quotes: --body 'your message here'
121
+ (single quotes prevent all shell interpretation)`)
105
122
  .action(async (token, options) => {
106
123
  const { sendMessage } = await import("./commands/worker.js");
107
124
  await sendMessage(token, options.name, options.body);
@@ -127,6 +144,15 @@ cronCmd
127
144
  .requiredOption("--message <message>", "Action to perform when job fires")
128
145
  .requiredOption("--respond-to <respondTo>", "Who to respond to when done")
129
146
  .option("--timezone <timezone>", "IANA timezone (e.g., 'America/New_York')")
147
+ .addHelpText("after", `
148
+ Warning:
149
+ The shell processes --message and --respond-to before the CLI receives them.
150
+ Be mindful of bash special characters in these values:
151
+ $var, \${x} variable expansion → escape as \\$ or use single quotes
152
+ \`cmd\`, $(x) command substitution → escape backticks or use single quotes
153
+ !, \\\\, " history/escape chars → escape or use single quotes
154
+ The safest option is always single quotes: --message 'your message here'
155
+ (single quotes prevent all shell interpretation)`)
130
156
  .action(async (token, options) => {
131
157
  const { createCron } = await import("./commands/worker.js");
132
158
  await createCron(token, options);
@@ -26,6 +26,7 @@ export declare class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase
26
26
  }): Promise<MessageRow[]>;
27
27
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
28
28
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
29
+ lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
29
30
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
30
31
  markMessageAsRead(id: number): Promise<MessageRow | null>;
31
32
  markMessageAsInjected(id: number): Promise<void>;
@@ -142,6 +142,21 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
142
142
  }
143
143
  return result;
144
144
  }
145
+ async lastMessageAtByCoworker(humanName) {
146
+ const rows = await this.sql `
147
+ SELECT
148
+ CASE WHEN from_name = ${humanName} THEN to_name ELSE from_name END AS coworker,
149
+ MAX(created_at) AS last_at
150
+ FROM messages
151
+ WHERE from_name = ${humanName} OR to_name = ${humanName}
152
+ GROUP BY coworker
153
+ `;
154
+ const result = new Map();
155
+ for (const row of rows) {
156
+ result.set(row.coworker, row.last_at);
157
+ }
158
+ return result;
159
+ }
145
160
  async createMessageImpl(from, to, body) {
146
161
  const [row] = await this.sql `
147
162
  INSERT INTO messages (from_name, to_name, body)
@@ -27,6 +27,7 @@ export declare class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
27
27
  }): Promise<MessageRow[]>;
28
28
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
29
29
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
30
+ lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
30
31
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
31
32
  markMessageAsRead(id: number): Promise<MessageRow | null>;
32
33
  markMessageAsInjected(id: number): Promise<void>;
@@ -197,6 +197,22 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
197
197
  }
198
198
  return result;
199
199
  }
200
+ async lastMessageAtByCoworker(humanName) {
201
+ const stmt = this.db.prepare(`
202
+ SELECT
203
+ CASE WHEN from_name = ? THEN to_name ELSE from_name END AS coworker,
204
+ MAX(created_at) AS last_at
205
+ FROM messages
206
+ WHERE from_name = ? OR to_name = ?
207
+ GROUP BY coworker
208
+ `);
209
+ const rows = stmt.all(humanName, humanName, humanName);
210
+ const result = new Map();
211
+ for (const row of rows) {
212
+ result.set(row.coworker, new Date(row.last_at + 'Z'));
213
+ }
214
+ return result;
215
+ }
200
216
  async createMessageImpl(from, to, body) {
201
217
  const stmt = this.db.prepare(`
202
218
  INSERT INTO messages (from_name, to_name, body)
@@ -28,6 +28,7 @@ export declare abstract class AgentOfficeStorageBase implements AgentOfficeStora
28
28
  }): Promise<MessageRow[]>;
29
29
  abstract listMessagesFromSender(name: string): Promise<MessageRow[]>;
30
30
  abstract countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
31
+ abstract lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
31
32
  abstract markMessageAsRead(id: number): Promise<MessageRow | null>;
32
33
  abstract markMessageAsInjected(id: number): Promise<void>;
33
34
  abstract markMessagesAsNotified(ids: number[]): Promise<void>;
@@ -37,6 +37,7 @@ export interface AgentOfficeStorage {
37
37
  }): Promise<MessageRow[]>;
38
38
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
39
39
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
40
+ lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
40
41
  createMessage(from: string, to: string, body: string): Promise<MessageRow>;
41
42
  markMessageAsRead(id: number): Promise<MessageRow | null>;
42
43
  markMessageAsInjected(id: number): Promise<void>;
@@ -69,6 +69,17 @@ export function generateSystemPrompt(name, status, humanName, humanDescription,
69
69
  ` --body "Your message here" \\`,
70
70
  ` ${token}`,
71
71
  ``,
72
+ ` If your message contains $ characters, escape them to avoid shell`,
73
+ ` variable expansion (bash expands unescaped $ before the CLI sees it):`,
74
+ ` agent-office worker send-message \\`,
75
+ ` --name alice \\`,
76
+ ` --body "The total cost is \\$42" \\`,
77
+ ` ${token}`,
78
+ ``,
79
+ ` ⚠ Always escape $ in --body when using double quotes in bash,`,
80
+ ` or switch to single quotes. Unescaped $ will be silently`,
81
+ ` expanded by the shell and your message will be garbled.`,
82
+ ``,
72
83
  ` Manage scheduled tasks (optional)`,
73
84
  ` agent-office worker cron \\`,
74
85
  ` ${token}`,
@@ -190,17 +201,34 @@ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler)
190
201
  try {
191
202
  const sessions = await storage.listSessions();
192
203
  const humanName = await storage.getConfig('human_name') ?? "Human";
193
- const unreadCounts = await storage.countUnreadBySender(humanName);
204
+ const [unreadCounts, lastMessageAt] = await Promise.all([
205
+ storage.countUnreadBySender(humanName),
206
+ storage.lastMessageAtByCoworker(humanName),
207
+ ]);
194
208
  const coworkers = [
195
- { name: humanName, status: null, isHuman: true, unreadMessages: 0 },
209
+ { name: humanName, status: null, isHuman: true, unreadMessages: 0, lastMessageAt: null },
196
210
  ...sessions.map(s => ({
197
211
  name: s.name,
198
212
  status: s.status,
199
213
  isHuman: false,
200
- unreadMessages: unreadCounts.get(s.name) ?? 0
214
+ unreadMessages: unreadCounts.get(s.name) ?? 0,
215
+ lastMessageAt: lastMessageAt.get(s.name)?.toISOString() ?? null,
201
216
  }))
202
217
  ];
203
- res.json(coworkers);
218
+ // Sort non-human coworkers by most recent message (most recent first),
219
+ // coworkers with no messages appear last (sorted by name as tiebreaker)
220
+ const [human, ...agents] = coworkers;
221
+ agents.sort((a, b) => {
222
+ if (a.lastMessageAt && b.lastMessageAt) {
223
+ return new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime();
224
+ }
225
+ if (a.lastMessageAt)
226
+ return -1;
227
+ if (b.lastMessageAt)
228
+ return 1;
229
+ return a.name.localeCompare(b.name);
230
+ });
231
+ res.json([human, ...agents]);
204
232
  }
205
233
  catch (err) {
206
234
  console.error("GET /coworkers error:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "An office for your AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",