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 +6 -0
- package/dist/cli.js +26 -0
- package/dist/db/postgresql-storage.d.ts +1 -0
- package/dist/db/postgresql-storage.js +15 -0
- package/dist/db/sqlite-storage.d.ts +1 -0
- package/dist/db/sqlite-storage.js +16 -0
- package/dist/db/storage-base.d.ts +1 -0
- package/dist/db/storage.d.ts +1 -0
- package/dist/server/routes.js +32 -4
- package/package.json +1 -1
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>;
|
package/dist/db/storage.d.ts
CHANGED
|
@@ -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>;
|
package/dist/server/routes.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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);
|