agent-office 0.0.3 → 0.0.6

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.
@@ -73,6 +73,24 @@ function generateWelcomeMessage(name, mode, status, humanName, humanDescription,
73
73
  ` agent-office worker cron \\`,
74
74
  ` ${token}`,
75
75
  ``,
76
+ ` Store a memory (persistent across sessions)`,
77
+ ` agent-office worker memory add \\`,
78
+ ` --content "your memory here" \\`,
79
+ ` ${token}`,
80
+ ``,
81
+ ` Search your memories`,
82
+ ` agent-office worker memory search \\`,
83
+ ` --query "your search" \\`,
84
+ ` ${token}`,
85
+ ``,
86
+ ` List all your memories`,
87
+ ` agent-office worker memory list \\`,
88
+ ` ${token}`,
89
+ ``,
90
+ ` Forget a memory`,
91
+ ` agent-office worker memory forget \\`,
92
+ ` ${token} <memory-id>`,
93
+ ``,
76
94
  `════════════════════════════════════════════════════════`,
77
95
  ` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
78
96
  `════════════════════════════════════════════════════════`,
@@ -109,7 +127,7 @@ function generateWelcomeMessage(name, mode, status, humanName, humanDescription,
109
127
  ``,
110
128
  ].join("\n");
111
129
  }
112
- export function createRouter(sql, opencode, serverUrl, scheduler) {
130
+ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager) {
113
131
  const router = Router();
114
132
  router.get("/health", (_req, res) => {
115
133
  res.json({ ok: true });
@@ -366,6 +384,47 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
366
384
  res.status(502).json({ error: "Failed to revert session", detail: String(err) });
367
385
  }
368
386
  });
387
+ router.post("/sessions/revert-all", async (_req, res) => {
388
+ const allSessions = await sql `
389
+ SELECT id, name, session_id, agent_code, mode, status, created_at FROM sessions
390
+ `;
391
+ const providers = await opencode.app.providers();
392
+ const defaultEntry = Object.entries(providers.default)[0];
393
+ if (!defaultEntry) {
394
+ res.status(502).json({ error: "No default model configured in OpenCode" });
395
+ return;
396
+ }
397
+ const results = [];
398
+ for (const session of allSessions) {
399
+ try {
400
+ const messages = await opencode.session.messages(session.session_id);
401
+ if (messages.length === 0) {
402
+ results.push({ name: session.name, ok: false, error: "No messages" });
403
+ continue;
404
+ }
405
+ const firstMessage = messages[0];
406
+ if (!firstMessage?.info?.id) {
407
+ results.push({ name: session.name, ok: false, error: "Failed to get first message ID" });
408
+ continue;
409
+ }
410
+ await opencode.session.revert(session.session_id, { messageID: firstMessage.info.id });
411
+ const clockInToken = `${session.agent_code}@${serverUrl}`;
412
+ 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}`;
413
+ await opencode.session.chat(session.session_id, {
414
+ modelID: defaultEntry[0],
415
+ providerID: defaultEntry[1],
416
+ parts: [{ type: "text", text: enrollmentMessage }],
417
+ });
418
+ results.push({ name: session.name, ok: true });
419
+ }
420
+ catch (err) {
421
+ console.error(`POST /sessions/revert-all error for "${session.name}":`, err);
422
+ results.push({ name: session.name, ok: false, error: String(err) });
423
+ }
424
+ }
425
+ const failed = results.filter((r) => !r.ok);
426
+ res.json({ ok: failed.length === 0, total: allSessions.length, results });
427
+ });
369
428
  router.delete("/sessions/:name", async (req, res) => {
370
429
  const { name } = req.params;
371
430
  const rows = await sql `
@@ -500,33 +559,34 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
500
559
  res.status(400).json({ error: "No valid recipients found" });
501
560
  return;
502
561
  }
562
+ const providers = await opencode.app.providers();
563
+ const defaultEntry = Object.entries(providers.default)[0];
503
564
  const results = [];
504
565
  for (const recipient of validRecipients) {
505
- let injected = false;
506
566
  const [msgRow] = await sql `
507
567
  INSERT INTO messages (from_name, to_name, body)
508
568
  VALUES (${trimmedFrom}, ${recipient}, ${trimmedBody})
509
569
  RETURNING id, from_name, to_name, body, read, injected, created_at
510
570
  `;
511
- if (sessionMap.has(recipient)) {
571
+ const msgId = msgRow.id;
572
+ let injected = false;
573
+ if (sessionMap.has(recipient) && defaultEntry) {
512
574
  const sessionId = sessionMap.get(recipient);
513
- const msgId = msgRow.id;
514
- injected = true;
515
- opencode.app.providers().then((providers) => {
516
- const defaultEntry = Object.entries(providers.default)[0];
517
- if (!defaultEntry)
518
- return;
519
- const injectText = `[Message from "${trimmedFrom}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
520
- return opencode.session.chat(sessionId, {
575
+ const injectText = `[Message from "${trimmedFrom}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
576
+ try {
577
+ await opencode.session.chat(sessionId, {
521
578
  modelID: defaultEntry[0],
522
579
  providerID: defaultEntry[1],
523
580
  parts: [{ type: "text", text: injectText }],
524
- }).then(() => sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`);
525
- }).catch((err) => {
526
- console.warn(`Warning: could not inject message into session ${recipient}:`, err);
527
- });
581
+ });
582
+ await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`;
583
+ injected = true;
584
+ }
585
+ catch (err) {
586
+ console.warn(`Warning: could not inject message into session "${recipient}":`, err);
587
+ }
528
588
  }
529
- results.push({ to: recipient, messageId: msgRow.id, injected });
589
+ results.push({ to: recipient, messageId: msgId, injected });
530
590
  }
531
591
  res.status(201).json({ ok: true, results });
532
592
  });
@@ -799,9 +859,135 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
799
859
  res.status(500).json({ error: "Internal server error" });
800
860
  }
801
861
  });
862
+ // ── Memory Endpoints (authenticated, for manage TUI) ───────────────────────
863
+ router.get("/sessions/:name/memories", async (req, res) => {
864
+ const { name } = req.params;
865
+ const limit = Math.min(parseInt(req.query.limit ?? "50", 10), 200);
866
+ const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
867
+ if (rows.length === 0) {
868
+ res.status(404).json({ error: `Session "${name}" not found` });
869
+ return;
870
+ }
871
+ try {
872
+ const memories = memoryManager.listMemories(name, limit);
873
+ const stats = memoryManager.getStats(name);
874
+ res.json({ memories, total: stats.total });
875
+ }
876
+ catch (err) {
877
+ console.error("GET /sessions/:name/memories error:", err);
878
+ res.status(500).json({ error: "Internal server error" });
879
+ }
880
+ });
881
+ router.post("/sessions/:name/memories", async (req, res) => {
882
+ const { name } = req.params;
883
+ const { content, metadata } = req.body;
884
+ if (!content || typeof content !== "string" || !content.trim()) {
885
+ res.status(400).json({ error: "content is required" });
886
+ return;
887
+ }
888
+ const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
889
+ if (rows.length === 0) {
890
+ res.status(404).json({ error: `Session "${name}" not found` });
891
+ return;
892
+ }
893
+ try {
894
+ const id = await memoryManager.addMemory(name, content.trim(), metadata ?? {});
895
+ res.status(201).json({ ok: true, id });
896
+ }
897
+ catch (err) {
898
+ console.error("POST /sessions/:name/memories error:", err);
899
+ res.status(500).json({ error: "Internal server error" });
900
+ }
901
+ });
902
+ router.get("/sessions/:name/memories/:memoryId", async (req, res) => {
903
+ const { name, memoryId } = req.params;
904
+ const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
905
+ if (rows.length === 0) {
906
+ res.status(404).json({ error: `Session "${name}" not found` });
907
+ return;
908
+ }
909
+ try {
910
+ const memory = memoryManager.getMemory(name, memoryId);
911
+ if (!memory) {
912
+ res.status(404).json({ error: "Memory not found" });
913
+ return;
914
+ }
915
+ res.json(memory);
916
+ }
917
+ catch (err) {
918
+ console.error("GET /sessions/:name/memories/:memoryId error:", err);
919
+ res.status(500).json({ error: "Internal server error" });
920
+ }
921
+ });
922
+ router.put("/sessions/:name/memories/:memoryId", async (req, res) => {
923
+ const { name, memoryId } = req.params;
924
+ const { content, metadata } = req.body;
925
+ if (!content || typeof content !== "string" || !content.trim()) {
926
+ res.status(400).json({ error: "content is required" });
927
+ return;
928
+ }
929
+ const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
930
+ if (rows.length === 0) {
931
+ res.status(404).json({ error: `Session "${name}" not found` });
932
+ return;
933
+ }
934
+ try {
935
+ const updated = await memoryManager.updateMemory(name, memoryId, content.trim(), metadata);
936
+ if (!updated) {
937
+ res.status(404).json({ error: "Memory not found" });
938
+ return;
939
+ }
940
+ res.json({ ok: true });
941
+ }
942
+ catch (err) {
943
+ console.error("PUT /sessions/:name/memories/:memoryId error:", err);
944
+ res.status(500).json({ error: "Internal server error" });
945
+ }
946
+ });
947
+ router.delete("/sessions/:name/memories/:memoryId", async (req, res) => {
948
+ const { name, memoryId } = req.params;
949
+ const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
950
+ if (rows.length === 0) {
951
+ res.status(404).json({ error: `Session "${name}" not found` });
952
+ return;
953
+ }
954
+ try {
955
+ const deleted = memoryManager.deleteMemory(name, memoryId);
956
+ if (!deleted) {
957
+ res.status(404).json({ error: "Memory not found" });
958
+ return;
959
+ }
960
+ res.json({ deleted: true, id: memoryId });
961
+ }
962
+ catch (err) {
963
+ console.error("DELETE /sessions/:name/memories/:memoryId error:", err);
964
+ res.status(500).json({ error: "Internal server error" });
965
+ }
966
+ });
967
+ router.post("/sessions/:name/memories/search", async (req, res) => {
968
+ const { name } = req.params;
969
+ const { query, limit } = req.body;
970
+ if (!query || typeof query !== "string" || !query.trim()) {
971
+ res.status(400).json({ error: "query is required" });
972
+ return;
973
+ }
974
+ const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
975
+ if (rows.length === 0) {
976
+ res.status(404).json({ error: `Session "${name}" not found` });
977
+ return;
978
+ }
979
+ try {
980
+ const results = await memoryManager.searchMemories(name, query.trim(), Math.min(limit ?? 10, 50));
981
+ res.json(results);
982
+ }
983
+ catch (err) {
984
+ console.error("POST /sessions/:name/memories/search error:", err);
985
+ res.status(500).json({ error: "Internal server error" });
986
+ }
987
+ });
802
988
  return router;
803
989
  }
804
- export function createWorkerRouter(sql, opencode, serverUrl) {
990
+ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
805
991
  const router = Router();
806
992
  router.get("/worker/clock-in", async (req, res) => {
807
993
  const { code } = req.query;
@@ -870,6 +1056,24 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
870
1056
  ` agent-office worker cron \\`,
871
1057
  ` ${token}`,
872
1058
  ``,
1059
+ ` Store a memory (persistent across sessions)`,
1060
+ ` agent-office worker memory add \\`,
1061
+ ` --content "your memory here" \\`,
1062
+ ` ${token}`,
1063
+ ``,
1064
+ ` Search your memories`,
1065
+ ` agent-office worker memory search \\`,
1066
+ ` --query "your search" \\`,
1067
+ ` ${token}`,
1068
+ ``,
1069
+ ` List all your memories`,
1070
+ ` agent-office worker memory list \\`,
1071
+ ` ${token}`,
1072
+ ``,
1073
+ ` Forget a memory`,
1074
+ ` agent-office worker memory forget \\`,
1075
+ ` ${token} <memory-id>`,
1076
+ ``,
873
1077
  `════════════════════════════════════════════════════════`,
874
1078
  ` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
875
1079
  `════════════════════════════════════════════════════════`,
@@ -1020,33 +1224,34 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
1020
1224
  res.status(400).json({ error: "No valid recipients found" });
1021
1225
  return;
1022
1226
  }
1227
+ const providers = await opencode.app.providers();
1228
+ const defaultEntry = Object.entries(providers.default)[0];
1023
1229
  const results = [];
1024
1230
  for (const recipient of validRecipients) {
1025
- let injected = false;
1026
1231
  const [msgRow] = await sql `
1027
1232
  INSERT INTO messages (from_name, to_name, body)
1028
1233
  VALUES (${session.name}, ${recipient}, ${trimmedBody})
1029
1234
  RETURNING id, from_name, to_name, body, read, injected, created_at
1030
1235
  `;
1031
- if (sessionMap.has(recipient)) {
1236
+ const msgId = msgRow.id;
1237
+ let injected = false;
1238
+ if (sessionMap.has(recipient) && defaultEntry) {
1032
1239
  const recipientSessionId = sessionMap.get(recipient);
1033
- const msgId = msgRow.id;
1034
- injected = true;
1035
- opencode.app.providers().then((providers) => {
1036
- const defaultEntry = Object.entries(providers.default)[0];
1037
- if (!defaultEntry)
1038
- return;
1039
- const injectText = `[Message from "${session.name}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
1040
- return opencode.session.chat(recipientSessionId, {
1240
+ const injectText = `[Message from "${session.name}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
1241
+ try {
1242
+ await opencode.session.chat(recipientSessionId, {
1041
1243
  modelID: defaultEntry[0],
1042
1244
  providerID: defaultEntry[1],
1043
1245
  parts: [{ type: "text", text: injectText }],
1044
- }).then(() => sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`);
1045
- }).catch((err) => {
1046
- console.warn(`Warning: could not inject message into session ${recipient}:`, err);
1047
- });
1246
+ });
1247
+ await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`;
1248
+ injected = true;
1249
+ }
1250
+ catch (err) {
1251
+ console.warn(`Warning: could not inject message into session "${recipient}":`, err);
1252
+ }
1048
1253
  }
1049
- results.push({ to: recipient, messageId: msgRow.id, injected });
1254
+ results.push({ to: recipient, messageId: msgId, injected });
1050
1255
  }
1051
1256
  res.status(201).json({ ok: true, results });
1052
1257
  });
@@ -1370,5 +1575,115 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
1370
1575
  res.status(500).json({ error: "Internal server error" });
1371
1576
  }
1372
1577
  });
1578
+ // ── Worker Memory Endpoints (authenticated via agent_code in query) ────────
1579
+ router.post("/worker/memory/add", async (req, res) => {
1580
+ const { code } = req.query;
1581
+ const { content, metadata } = req.body;
1582
+ if (!code || typeof code !== "string") {
1583
+ res.status(400).json({ error: "code query parameter is required" });
1584
+ return;
1585
+ }
1586
+ if (!content || typeof content !== "string" || !content.trim()) {
1587
+ res.status(400).json({ error: "content is required" });
1588
+ return;
1589
+ }
1590
+ const session = await sql `
1591
+ SELECT name FROM sessions WHERE agent_code = ${code}
1592
+ `;
1593
+ if (session.length === 0) {
1594
+ res.status(401).json({ error: "Invalid agent code" });
1595
+ return;
1596
+ }
1597
+ const sessionName = session[0].name;
1598
+ try {
1599
+ const id = await memoryManager.addMemory(sessionName, content.trim(), metadata ?? {});
1600
+ res.status(201).json({ ok: true, id });
1601
+ }
1602
+ catch (err) {
1603
+ console.error("POST /worker/memory/add error:", err);
1604
+ res.status(500).json({ error: "Internal server error" });
1605
+ }
1606
+ });
1607
+ router.post("/worker/memory/search", async (req, res) => {
1608
+ const { code } = req.query;
1609
+ const { query, limit } = req.body;
1610
+ if (!code || typeof code !== "string") {
1611
+ res.status(400).json({ error: "code query parameter is required" });
1612
+ return;
1613
+ }
1614
+ if (!query || typeof query !== "string" || !query.trim()) {
1615
+ res.status(400).json({ error: "query is required" });
1616
+ return;
1617
+ }
1618
+ const session = await sql `
1619
+ SELECT name FROM sessions WHERE agent_code = ${code}
1620
+ `;
1621
+ if (session.length === 0) {
1622
+ res.status(401).json({ error: "Invalid agent code" });
1623
+ return;
1624
+ }
1625
+ const sessionName = session[0].name;
1626
+ try {
1627
+ const results = await memoryManager.searchMemories(sessionName, query.trim(), Math.min(limit ?? 10, 50));
1628
+ res.json(results);
1629
+ }
1630
+ catch (err) {
1631
+ console.error("POST /worker/memory/search error:", err);
1632
+ res.status(500).json({ error: "Internal server error" });
1633
+ }
1634
+ });
1635
+ router.get("/worker/memory/list", async (req, res) => {
1636
+ const { code, limit: limitStr } = req.query;
1637
+ if (!code || typeof code !== "string") {
1638
+ res.status(400).json({ error: "code query parameter is required" });
1639
+ return;
1640
+ }
1641
+ const session = await sql `
1642
+ SELECT name FROM sessions WHERE agent_code = ${code}
1643
+ `;
1644
+ if (session.length === 0) {
1645
+ res.status(401).json({ error: "Invalid agent code" });
1646
+ return;
1647
+ }
1648
+ const sessionName = session[0].name;
1649
+ const limit = Math.min(parseInt(limitStr ?? "50", 10), 200);
1650
+ try {
1651
+ const memories = memoryManager.listMemories(sessionName, limit);
1652
+ const stats = memoryManager.getStats(sessionName);
1653
+ res.json({ memories, total: stats.total });
1654
+ }
1655
+ catch (err) {
1656
+ console.error("GET /worker/memory/list error:", err);
1657
+ res.status(500).json({ error: "Internal server error" });
1658
+ }
1659
+ });
1660
+ router.delete("/worker/memory/:memoryId", async (req, res) => {
1661
+ const { code } = req.query;
1662
+ const { memoryId } = req.params;
1663
+ if (!code || typeof code !== "string") {
1664
+ res.status(400).json({ error: "code query parameter is required" });
1665
+ return;
1666
+ }
1667
+ const session = await sql `
1668
+ SELECT name FROM sessions WHERE agent_code = ${code}
1669
+ `;
1670
+ if (session.length === 0) {
1671
+ res.status(401).json({ error: "Invalid agent code" });
1672
+ return;
1673
+ }
1674
+ const sessionName = session[0].name;
1675
+ try {
1676
+ const deleted = memoryManager.deleteMemory(sessionName, memoryId);
1677
+ if (!deleted) {
1678
+ res.status(404).json({ error: "Memory not found" });
1679
+ return;
1680
+ }
1681
+ res.json({ deleted: true, id: memoryId });
1682
+ }
1683
+ catch (err) {
1684
+ console.error("DELETE /worker/memory/:memoryId error:", err);
1685
+ res.status(500).json({ error: "Internal server error" });
1686
+ }
1687
+ });
1373
1688
  return router;
1374
1689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
4
4
  "description": "Manage OpenCode sessions with named aliases",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,6 +23,7 @@
23
23
  "dev:serve": "tsx --watch src/cli.ts serve",
24
24
  "dev:manage": "tsx src/cli.ts manage",
25
25
  "dev:worker": "tsx src/cli.ts worker",
26
+ "dev:communicator": "tsx src/cli.ts communicator web",
26
27
  "build": "tsc",
27
28
  "clean": "rm -rf dist",
28
29
  "prepublishOnly": "npm run clean && npm run build"
@@ -38,11 +39,13 @@
38
39
  "croner": "^10.0.1",
39
40
  "dotenv": "^16.0.0",
40
41
  "express": "^4.18.0",
42
+ "fastmemory": "^0.1.5",
41
43
  "ink": "^5.0.0",
42
44
  "postgres": "^3.4.0",
43
45
  "react": "^18.0.0"
44
46
  },
45
47
  "devDependencies": {
48
+ "@types/better-sqlite3": "^7.6.13",
46
49
  "@types/express": "^4.17.0",
47
50
  "@types/node": "^20.0.0",
48
51
  "@types/react": "^18.0.0",