quadwork 1.6.3 → 1.8.0
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 -1
- package/bin/quadwork.js +42 -59
- package/bridges/discord/__pycache__/discord_bridge.cpython-314.pyc +0 -0
- package/bridges/discord/discord_bridge.py +63 -18
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +3 -3
- package/out/__next._full.txt +14 -14
- package/out/__next._head.txt +4 -4
- package/out/__next._index.txt +7 -7
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/KleEGCyPe1ovSiWKr9GnB/_ssgManifest.js +1 -0
- package/out/_next/static/chunks/{0ftcj9qreh~p4.js → 0_y-97xg1l.bn.js} +13 -13
- package/out/_next/static/chunks/{0-jj8tpbs48x4.js → 0cploqtj5jy4z.js} +1 -1
- package/out/_next/static/chunks/0koy9hplvko3w.css +2 -0
- package/out/_next/static/chunks/14zyqqpdz2x7j.js +1 -0
- package/out/_next/static/chunks/{0o3_.p5ivp5sp.js → 152f2hu-ivy6f.js} +1 -1
- package/out/_next/static/media/favicon.05o2q2p4kvnq_.ico +0 -0
- package/out/_not-found/__next._full.txt +13 -13
- package/out/_not-found/__next._head.txt +4 -4
- package/out/_not-found/__next._index.txt +7 -7
- package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +13 -13
- package/out/app-shell/__next._full.txt +13 -13
- package/out/app-shell/__next._head.txt +4 -4
- package/out/app-shell/__next._index.txt +7 -7
- package/out/app-shell/__next._tree.txt +2 -2
- package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
- package/out/app-shell/__next.app-shell.txt +3 -3
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +13 -13
- package/out/favicon.ico +0 -0
- package/out/icon.png +0 -0
- package/out/icon.svg +11 -4
- package/out/index.html +1 -1
- package/out/index.txt +14 -14
- package/out/project/_/__next._full.txt +14 -14
- package/out/project/_/__next._head.txt +4 -4
- package/out/project/_/__next._index.txt +7 -7
- package/out/project/_/__next._tree.txt +2 -2
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
- package/out/project/_/__next.project.$d$id.txt +3 -3
- package/out/project/_/__next.project.txt +3 -3
- package/out/project/_/queue/__next._full.txt +14 -14
- package/out/project/_/queue/__next._head.txt +4 -4
- package/out/project/_/queue/__next._index.txt +7 -7
- package/out/project/_/queue/__next._tree.txt +2 -2
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
- package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
- package/out/project/_/queue/__next.project.$d$id.txt +3 -3
- package/out/project/_/queue/__next.project.txt +3 -3
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +14 -14
- package/out/project/_.html +1 -1
- package/out/project/_.txt +14 -14
- package/out/quadwork-symbol.svg +15 -0
- package/out/settings/__next._full.txt +14 -14
- package/out/settings/__next._head.txt +4 -4
- package/out/settings/__next._index.txt +7 -7
- package/out/settings/__next._tree.txt +2 -2
- package/out/settings/__next.settings.__PAGE__.txt +3 -3
- package/out/settings/__next.settings.txt +3 -3
- package/out/settings.html +1 -1
- package/out/settings.txt +14 -14
- package/out/setup/__next._full.txt +14 -14
- package/out/setup/__next._head.txt +4 -4
- package/out/setup/__next._index.txt +7 -7
- package/out/setup/__next._tree.txt +2 -2
- package/out/setup/__next.setup.__PAGE__.txt +3 -3
- package/out/setup/__next.setup.txt +3 -3
- package/out/setup.html +1 -1
- package/out/setup.txt +14 -14
- package/package.json +2 -1
- package/server/index.js +77 -20
- package/server/queue-watcher.js +20 -11
- package/server/routes.discordBridge.test.js +5 -5
- package/server/routes.js +141 -212
- package/server/routes.telegramBridge.test.js +6 -6
- package/templates/config.toml +7 -7
- package/out/_next/static/ZT6D996Dw9auBgcm_HHTY/_ssgManifest.js +0 -1
- package/out/_next/static/chunks/0j-zyy6.adwtl.css +0 -2
- package/out/_next/static/chunks/0n7b.b.q4nmo..js +0 -1
- package/out/_next/static/chunks/0~xrqi87fqraz.js +0 -1
- package/out/_next/static/chunks/12i404gkhv7q..js +0 -4
- package/out/_next/static/media/favicon.0qzfoe774zb3r.ico +0 -0
- package/out/project/_/memory/__next._full.txt +0 -21
- package/out/project/_/memory/__next._head.txt +0 -6
- package/out/project/_/memory/__next._index.txt +0 -8
- package/out/project/_/memory/__next._tree.txt +0 -3
- package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +0 -6
- package/out/project/_/memory/__next.project.$d$id.memory.txt +0 -5
- package/out/project/_/memory/__next.project.$d$id.txt +0 -5
- package/out/project/_/memory/__next.project.txt +0 -5
- package/out/project/_/memory.html +0 -1
- package/out/project/_/memory.txt +0 -21
- /package/out/_next/static/{ZT6D996Dw9auBgcm_HHTY → KleEGCyPe1ovSiWKr9GnB}/_buildManifest.js +0 -0
- /package/out/_next/static/{ZT6D996Dw9auBgcm_HHTY → KleEGCyPe1ovSiWKr9GnB}/_clientMiddlewareManifest.js +0 -0
package/server/routes.js
CHANGED
|
@@ -8,6 +8,8 @@ const fs = require("fs");
|
|
|
8
8
|
const path = require("path");
|
|
9
9
|
const os = require("os");
|
|
10
10
|
|
|
11
|
+
const multer = require("multer");
|
|
12
|
+
|
|
11
13
|
const router = express.Router();
|
|
12
14
|
|
|
13
15
|
const CONFIG_DIR = path.join(os.homedir(), ".quadwork");
|
|
@@ -113,18 +115,32 @@ function chatAuthHeaders(token) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
router.get("/api/chat", async (req, res) => {
|
|
118
|
+
const projectId = req.query.project;
|
|
116
119
|
const apiPath = req.query.path || "/api/messages";
|
|
117
|
-
const { url: base, token } = getChattrConfig(
|
|
120
|
+
const { url: base, token } = getChattrConfig(projectId);
|
|
118
121
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const buildUrl = (tok) => {
|
|
123
|
+
const fwd = new URLSearchParams();
|
|
124
|
+
for (const [k, v] of Object.entries(req.query)) {
|
|
125
|
+
if (k !== "path") fwd.set(k, String(v));
|
|
126
|
+
}
|
|
127
|
+
if (tok) fwd.set("token", tok);
|
|
128
|
+
return `${base}${apiPath}?${fwd.toString()}`;
|
|
129
|
+
};
|
|
124
130
|
|
|
125
|
-
const url = `${base}${apiPath}?${fwd.toString()}`;
|
|
126
131
|
try {
|
|
127
|
-
const r = await fetch(
|
|
132
|
+
const r = await fetch(buildUrl(token), { headers: chatAuthHeaders(token) });
|
|
133
|
+
// #448: on 401/403, re-sync the session token from AC and retry
|
|
134
|
+
// once. The stored token may be stale after an AC restart.
|
|
135
|
+
if ((r.status === 401 || r.status === 403) && projectId) {
|
|
136
|
+
try { await syncChattrToken(projectId); } catch {}
|
|
137
|
+
const { token: refreshed } = getChattrConfig(projectId);
|
|
138
|
+
if (refreshed && refreshed !== token) {
|
|
139
|
+
const retry = await fetch(buildUrl(refreshed), { headers: chatAuthHeaders(refreshed) });
|
|
140
|
+
if (!retry.ok) return res.status(retry.status).json({ error: `AgentChattr returned ${retry.status}` });
|
|
141
|
+
return res.json(await retry.json());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
128
144
|
if (!r.ok) return res.status(r.status).json({ error: `AgentChattr returned ${r.status}` });
|
|
129
145
|
res.json(await r.json());
|
|
130
146
|
} catch (err) {
|
|
@@ -977,6 +993,53 @@ router.post("/api/chat", async (req, res) => {
|
|
|
977
993
|
}
|
|
978
994
|
});
|
|
979
995
|
|
|
996
|
+
// ─── Image upload (#466) ──────────────────────────────────────────────────
|
|
997
|
+
|
|
998
|
+
const UPLOAD_MAX_BYTES = 10 * 1024 * 1024; // 10MB
|
|
999
|
+
const ALLOWED_MIME = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
1000
|
+
const uploadStorage = multer.diskStorage({
|
|
1001
|
+
destination: (req, _file, cb) => {
|
|
1002
|
+
const projectId = req.query.project || "";
|
|
1003
|
+
if (!projectId || /[/\\]/.test(projectId)) return cb(new Error("Invalid project"));
|
|
1004
|
+
const dir = path.join(CONFIG_DIR, projectId, "uploads");
|
|
1005
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1006
|
+
cb(null, dir);
|
|
1007
|
+
},
|
|
1008
|
+
filename: (_req, file, cb) => {
|
|
1009
|
+
const ext = path.extname(file.originalname) || ".png";
|
|
1010
|
+
cb(null, `upload-${Date.now()}${ext}`);
|
|
1011
|
+
},
|
|
1012
|
+
});
|
|
1013
|
+
const upload = multer({
|
|
1014
|
+
storage: uploadStorage,
|
|
1015
|
+
limits: { fileSize: UPLOAD_MAX_BYTES },
|
|
1016
|
+
fileFilter: (_req, file, cb) => {
|
|
1017
|
+
if (ALLOWED_MIME.has(file.mimetype)) cb(null, true);
|
|
1018
|
+
else cb(new Error(`Unsupported type: ${file.mimetype}`));
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
router.post("/api/upload", upload.single("file"), (req, res) => {
|
|
1023
|
+
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
|
|
1024
|
+
return res.json({
|
|
1025
|
+
ok: true,
|
|
1026
|
+
path: req.file.path,
|
|
1027
|
+
name: req.file.filename,
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// Serve uploaded images for thumbnail rendering
|
|
1032
|
+
router.get("/api/uploads/:project/:filename", (req, res) => {
|
|
1033
|
+
const { project, filename } = req.params;
|
|
1034
|
+
// Sanitize to prevent directory traversal
|
|
1035
|
+
if (/[/\\]/.test(project) || /[/\\]/.test(filename)) {
|
|
1036
|
+
return res.status(400).json({ error: "Invalid path" });
|
|
1037
|
+
}
|
|
1038
|
+
const filePath = path.join(CONFIG_DIR, project, "uploads", filename);
|
|
1039
|
+
if (!fs.existsSync(filePath)) return res.status(404).json({ error: "Not found" });
|
|
1040
|
+
res.sendFile(filePath);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
980
1043
|
// ─── Projects (dashboard aggregation) ──────────────────────────────────────
|
|
981
1044
|
|
|
982
1045
|
function ghJson(args) {
|
|
@@ -1674,184 +1737,7 @@ router.get("/api/batch-progress", async (req, res) => {
|
|
|
1674
1737
|
res.json(data);
|
|
1675
1738
|
});
|
|
1676
1739
|
|
|
1677
|
-
//
|
|
1678
|
-
|
|
1679
|
-
function getProject(projectId) {
|
|
1680
|
-
try {
|
|
1681
|
-
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
1682
|
-
return cfg.projects?.find((p) => p.id === projectId) || null;
|
|
1683
|
-
} catch {
|
|
1684
|
-
return null;
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
function getMemoryPaths(project) {
|
|
1689
|
-
const workDir = project.working_dir || "";
|
|
1690
|
-
return {
|
|
1691
|
-
cardsDir: project.memory_cards_dir || path.join(workDir, "..", "agent-memory", "archive", "v2", "cards"),
|
|
1692
|
-
sharedMemoryPath: project.shared_memory_path || path.join(workDir, "..", "agent-memory", "central", "short-term", "agent-os.md"),
|
|
1693
|
-
butlerDir: project.butler_scripts_dir || path.join(workDir, "..", "agent-memory", "scripts"),
|
|
1694
|
-
};
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
function findMdFiles(dir) {
|
|
1698
|
-
const results = [];
|
|
1699
|
-
if (!fs.existsSync(dir)) return results;
|
|
1700
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1701
|
-
const full = path.join(dir, entry.name);
|
|
1702
|
-
if (entry.isDirectory()) results.push(...findMdFiles(full));
|
|
1703
|
-
else if (entry.name.endsWith(".md")) results.push(full);
|
|
1704
|
-
}
|
|
1705
|
-
return results;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
function parseFrontmatter(content) {
|
|
1709
|
-
const fm = {};
|
|
1710
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1711
|
-
if (!match) return fm;
|
|
1712
|
-
for (const line of match[1].split("\n")) {
|
|
1713
|
-
const idx = line.indexOf(":");
|
|
1714
|
-
if (idx > 0) {
|
|
1715
|
-
const key = line.slice(0, idx).trim();
|
|
1716
|
-
let val = line.slice(idx + 1).trim();
|
|
1717
|
-
if (val.startsWith("[") && val.endsWith("]")) val = val.slice(1, -1).trim();
|
|
1718
|
-
fm[key] = val;
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
return fm;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
router.get("/api/memory", (req, res) => {
|
|
1725
|
-
const projectId = req.query.project || "";
|
|
1726
|
-
const action = req.query.action || "cards";
|
|
1727
|
-
const project = getProject(projectId);
|
|
1728
|
-
if (!project) return res.status(404).json({ error: "Project not found" });
|
|
1729
|
-
|
|
1730
|
-
const paths = getMemoryPaths(project);
|
|
1731
|
-
|
|
1732
|
-
if (action === "cards") {
|
|
1733
|
-
const search = req.query.search || "";
|
|
1734
|
-
try {
|
|
1735
|
-
const files = findMdFiles(paths.cardsDir);
|
|
1736
|
-
const cards = files.map((fullPath) => {
|
|
1737
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
1738
|
-
const fm = parseFrontmatter(content);
|
|
1739
|
-
const relPath = path.relative(paths.cardsDir, fullPath);
|
|
1740
|
-
const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "").trim();
|
|
1741
|
-
const firstLine = body.split("\n")[0]?.replace(/^#\s*/, "").trim();
|
|
1742
|
-
return {
|
|
1743
|
-
file: relPath,
|
|
1744
|
-
title: firstLine || fm.id || path.basename(fullPath, ".md"),
|
|
1745
|
-
date: fm.at || "",
|
|
1746
|
-
agent: fm.by || "",
|
|
1747
|
-
tags: fm.tags || "",
|
|
1748
|
-
content: body,
|
|
1749
|
-
};
|
|
1750
|
-
});
|
|
1751
|
-
cards.sort((a, b) => b.date.localeCompare(a.date));
|
|
1752
|
-
if (search) {
|
|
1753
|
-
const q = search.toLowerCase();
|
|
1754
|
-
return res.json(cards.filter((c) =>
|
|
1755
|
-
c.title.toLowerCase().includes(q) || c.agent.toLowerCase().includes(q) || c.tags.toLowerCase().includes(q) || c.content.toLowerCase().includes(q)
|
|
1756
|
-
));
|
|
1757
|
-
}
|
|
1758
|
-
return res.json(cards);
|
|
1759
|
-
} catch {
|
|
1760
|
-
return res.json([]);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
if (action === "status") {
|
|
1765
|
-
const agents = project.agents || {};
|
|
1766
|
-
const status = {};
|
|
1767
|
-
for (const [id, agent] of Object.entries(agents)) {
|
|
1768
|
-
const targetPath = path.join(agent.cwd || "", "shared-memory.md");
|
|
1769
|
-
if (fs.existsSync(targetPath)) {
|
|
1770
|
-
const stat = fs.statSync(targetPath);
|
|
1771
|
-
status[id] = { injected: true, lastModified: stat.mtime.toISOString() };
|
|
1772
|
-
} else {
|
|
1773
|
-
status[id] = { injected: false, lastModified: null };
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
const sourceExists = fs.existsSync(paths.sharedMemoryPath);
|
|
1777
|
-
return res.json({ agents: status, sourceExists });
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
if (action === "shared-memory") {
|
|
1781
|
-
try {
|
|
1782
|
-
const content = fs.readFileSync(paths.sharedMemoryPath, "utf-8");
|
|
1783
|
-
return res.json({ content, path: paths.sharedMemoryPath });
|
|
1784
|
-
} catch {
|
|
1785
|
-
return res.json({ content: "", path: paths.sharedMemoryPath });
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
if (action === "settings") {
|
|
1790
|
-
return res.json({
|
|
1791
|
-
memory_cards_dir: project.memory_cards_dir || "",
|
|
1792
|
-
shared_memory_path: project.shared_memory_path || "",
|
|
1793
|
-
butler_scripts_dir: project.butler_scripts_dir || "",
|
|
1794
|
-
});
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
res.status(400).json({ error: "Unknown action" });
|
|
1798
|
-
});
|
|
1799
|
-
|
|
1800
|
-
router.post("/api/memory", (req, res) => {
|
|
1801
|
-
const projectId = req.query.project || "";
|
|
1802
|
-
const action = req.query.action || "";
|
|
1803
|
-
const project = getProject(projectId);
|
|
1804
|
-
if (!project) return res.status(404).json({ error: "Project not found" });
|
|
1805
|
-
|
|
1806
|
-
const paths = getMemoryPaths(project);
|
|
1807
|
-
|
|
1808
|
-
if (action === "butler") {
|
|
1809
|
-
const allowed = ["butler-scan.sh", "butler-consolidate.sh", "inject.sh"];
|
|
1810
|
-
const command = req.body.command;
|
|
1811
|
-
if (!allowed.includes(command)) return res.json({ ok: false, error: `Unknown command: ${command}` });
|
|
1812
|
-
const scriptPath = path.join(paths.butlerDir, command);
|
|
1813
|
-
if (!fs.existsSync(scriptPath)) return res.json({ ok: false, error: `Script not found: ${scriptPath}` });
|
|
1814
|
-
try {
|
|
1815
|
-
const output = execFileSync("bash", [scriptPath], {
|
|
1816
|
-
encoding: "utf-8",
|
|
1817
|
-
timeout: 30000,
|
|
1818
|
-
cwd: path.dirname(paths.butlerDir),
|
|
1819
|
-
});
|
|
1820
|
-
return res.json({ ok: true, output });
|
|
1821
|
-
} catch (err) {
|
|
1822
|
-
return res.json({ ok: false, error: err.message });
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
if (action === "save-memory") {
|
|
1827
|
-
try {
|
|
1828
|
-
const dir = path.dirname(paths.sharedMemoryPath);
|
|
1829
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1830
|
-
fs.writeFileSync(paths.sharedMemoryPath, req.body.content);
|
|
1831
|
-
return res.json({ ok: true });
|
|
1832
|
-
} catch (err) {
|
|
1833
|
-
return res.json({ ok: false, error: err.message });
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
if (action === "save-settings") {
|
|
1838
|
-
try {
|
|
1839
|
-
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
1840
|
-
const proj = cfg.projects?.find((p) => p.id === projectId);
|
|
1841
|
-
if (!proj) return res.json({ ok: false, error: "Project not found" });
|
|
1842
|
-
const s = req.body;
|
|
1843
|
-
if (s.memory_cards_dir !== undefined) proj.memory_cards_dir = s.memory_cards_dir || undefined;
|
|
1844
|
-
if (s.shared_memory_path !== undefined) proj.shared_memory_path = s.shared_memory_path || undefined;
|
|
1845
|
-
if (s.butler_scripts_dir !== undefined) proj.butler_scripts_dir = s.butler_scripts_dir || undefined;
|
|
1846
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
|
|
1847
|
-
return res.json({ ok: true });
|
|
1848
|
-
} catch (err) {
|
|
1849
|
-
return res.json({ ok: false, error: err.message });
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
res.status(400).json({ error: "Unknown action" });
|
|
1854
|
-
});
|
|
1740
|
+
// #445: Memory section (agent-memory butler integration) removed.
|
|
1855
1741
|
|
|
1856
1742
|
// ─── Setup ─────────────────────────────────────────────────────────────────
|
|
1857
1743
|
|
|
@@ -2378,9 +2264,12 @@ router.post("/api/rename", (req, res) => {
|
|
|
2378
2264
|
// ─── Telegram ──────────────────────────────────────────────────────────────
|
|
2379
2265
|
|
|
2380
2266
|
const BRIDGE_DIR = path.join(CONFIG_DIR, "agentchattr-telegram");
|
|
2267
|
+
// #444: pin agentchattr-telegram to a known commit (same pattern as
|
|
2268
|
+
// AGENTCHATTR_PIN in bin/quadwork.js for bcurts/agentchattr).
|
|
2269
|
+
const AGENTCHATTR_TELEGRAM_PIN = "4a6b45f1794c612328b9d5ee6d6fcb3f77015abc";
|
|
2381
2270
|
|
|
2382
2271
|
function telegramPidFile(projectId) {
|
|
2383
|
-
return path.join(CONFIG_DIR, `
|
|
2272
|
+
return path.join(CONFIG_DIR, `tg-bridge-${projectId}.pid`);
|
|
2384
2273
|
}
|
|
2385
2274
|
|
|
2386
2275
|
function telegramConfigToml(projectId) {
|
|
@@ -2388,7 +2277,7 @@ function telegramConfigToml(projectId) {
|
|
|
2388
2277
|
}
|
|
2389
2278
|
|
|
2390
2279
|
// #383: path to a project's AgentChattr config.toml. The install
|
|
2391
|
-
// handler patches this file to declare the `
|
|
2280
|
+
// handler patches this file to declare the `tg` agent
|
|
2392
2281
|
// so AC's registry accepts the bridge's register call.
|
|
2393
2282
|
function projectAgentchattrConfigPath(projectId) {
|
|
2394
2283
|
return path.join(CONFIG_DIR, projectId, "agentchattr", "config.toml");
|
|
@@ -2415,7 +2304,12 @@ function resolveProjectAgentchattrUrl(cfg, project) {
|
|
|
2415
2304
|
// AC message IDs advances the cursor past the other project's range,
|
|
2416
2305
|
// silently killing AC→TG forwarding for that project.
|
|
2417
2306
|
function buildTelegramBridgeToml(tg, projectId) {
|
|
2418
|
-
const cursorFile = path.join(CONFIG_DIR, `
|
|
2307
|
+
const cursorFile = path.join(CONFIG_DIR, `tg-bridge-cursor-${projectId}.json`);
|
|
2308
|
+
// #439: migrate old cursor file so the bridge doesn't replay history
|
|
2309
|
+
const oldCursor = path.join(CONFIG_DIR, `telegram-bridge-cursor-${projectId}.json`);
|
|
2310
|
+
if (!fs.existsSync(cursorFile) && fs.existsSync(oldCursor)) {
|
|
2311
|
+
fs.renameSync(oldCursor, cursorFile);
|
|
2312
|
+
}
|
|
2419
2313
|
return (
|
|
2420
2314
|
`[telegram]\n` +
|
|
2421
2315
|
`bot_token = "${tg.bot_token}"\n` +
|
|
@@ -2427,14 +2321,18 @@ function buildTelegramBridgeToml(tg, projectId) {
|
|
|
2427
2321
|
|
|
2428
2322
|
// #383 Bug 3: AC's registry rejects any base name not pre-declared
|
|
2429
2323
|
// in config.toml with `400 unknown base`. The bridge registers as
|
|
2430
|
-
// `telegram-bridge
|
|
2431
|
-
// Idempotent: only appends if the section
|
|
2324
|
+
// `tg` (#439: renamed from `telegram-bridge`), so every per-project
|
|
2325
|
+
// AC config must declare it. Idempotent: only appends if the section
|
|
2326
|
+
// is not already present. Also migrates old `[agents.telegram-bridge]`.
|
|
2432
2327
|
function patchAgentchattrConfigForTelegramBridge(tomlText) {
|
|
2433
|
-
if
|
|
2434
|
-
|
|
2328
|
+
// #439: migrate old slug if present
|
|
2329
|
+
const original = tomlText;
|
|
2330
|
+
tomlText = tomlText.replace(/^\[agents\.telegram-bridge\]\s*$/m, "[agents.tg]");
|
|
2331
|
+
if (/^\[agents\.tg\]\s*$/m.test(tomlText)) {
|
|
2332
|
+
return { text: tomlText, changed: tomlText !== original };
|
|
2435
2333
|
}
|
|
2436
2334
|
const sep = tomlText.length === 0 || tomlText.endsWith("\n") ? "" : "\n";
|
|
2437
|
-
const block = `\n[agents.
|
|
2335
|
+
const block = `\n[agents.tg]\nlabel = "Telegram Bridge"\n`;
|
|
2438
2336
|
return { text: tomlText + sep + block, changed: true };
|
|
2439
2337
|
}
|
|
2440
2338
|
|
|
@@ -2456,7 +2354,7 @@ function buildTelegramBridgeSpawnEnv(parentEnv) {
|
|
|
2456
2354
|
// config parse, auth failure) are recoverable instead of
|
|
2457
2355
|
// /dev/null'd by `stdio: "ignore"`.
|
|
2458
2356
|
function telegramBridgeLog(projectId) {
|
|
2459
|
-
return path.join(CONFIG_DIR, `
|
|
2357
|
+
return path.join(CONFIG_DIR, `tg-bridge-${projectId}.log`);
|
|
2460
2358
|
}
|
|
2461
2359
|
|
|
2462
2360
|
// Tail the last N lines of a file without reading the whole thing
|
|
@@ -2676,6 +2574,12 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2676
2574
|
try {
|
|
2677
2575
|
if (!fs.existsSync(BRIDGE_DIR)) {
|
|
2678
2576
|
execFileSync("gh", ["repo", "clone", "realproject7/agentchattr-telegram", BRIDGE_DIR], { encoding: "utf-8", timeout: 30000 });
|
|
2577
|
+
// #444: pin to a known commit after clone
|
|
2578
|
+
try {
|
|
2579
|
+
execFileSync("git", ["-C", BRIDGE_DIR, "checkout", "-B", "pinned", AGENTCHATTR_TELEGRAM_PIN], { encoding: "utf-8", timeout: 30000 });
|
|
2580
|
+
} catch {
|
|
2581
|
+
console.warn(`[telegram] WARNING: could not check out agentchattr-telegram pin ${AGENTCHATTR_TELEGRAM_PIN}; falling back to default branch.`);
|
|
2582
|
+
}
|
|
2679
2583
|
}
|
|
2680
2584
|
// #380: create the dedicated venv if missing. `python3 -m venv`
|
|
2681
2585
|
// builds a fresh isolated environment that bypasses PEP 668
|
|
@@ -2705,16 +2609,14 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2705
2609
|
`pip output tail:\n${pipOutput.split("\n").slice(-10).join("\n")}`,
|
|
2706
2610
|
});
|
|
2707
2611
|
}
|
|
2708
|
-
// #383 Bug 3: ensure every known project's AC config
|
|
2709
|
-
// the `
|
|
2710
|
-
//
|
|
2711
|
-
//
|
|
2712
|
-
// Idempotent — append-only, skips configs that already have
|
|
2713
|
-
// the section. Does NOT restart AC servers; the operator
|
|
2714
|
-
// must click SERVER → Restart to load the new agent slug.
|
|
2612
|
+
// #383 Bug 3 / #457: ensure every known project's AC config
|
|
2613
|
+
// declares the `tg` agent and migrates old `telegram-bridge`
|
|
2614
|
+
// slug. Restarts AC for projects whose config changed so the
|
|
2615
|
+
// new slug loads immediately.
|
|
2715
2616
|
const patched = [];
|
|
2716
2617
|
try {
|
|
2717
2618
|
const cfgAll = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
2619
|
+
const serverPort = cfgAll.port || 8400;
|
|
2718
2620
|
for (const proj of cfgAll.projects || []) {
|
|
2719
2621
|
if (!proj || !proj.id) continue;
|
|
2720
2622
|
const acPath = projectAgentchattrConfigPath(proj.id);
|
|
@@ -2725,6 +2627,14 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2725
2627
|
if (changed) {
|
|
2726
2628
|
fs.writeFileSync(acPath, text);
|
|
2727
2629
|
patched.push(proj.id);
|
|
2630
|
+
// #457: restart AC so it loads the new agent slug
|
|
2631
|
+
setTimeout(async () => {
|
|
2632
|
+
try {
|
|
2633
|
+
await fetch(`http://127.0.0.1:${serverPort}/api/agentchattr/${encodeURIComponent(proj.id)}/restart`, {
|
|
2634
|
+
method: "POST",
|
|
2635
|
+
});
|
|
2636
|
+
} catch {}
|
|
2637
|
+
}, 1000);
|
|
2728
2638
|
}
|
|
2729
2639
|
} catch {}
|
|
2730
2640
|
}
|
|
@@ -2855,7 +2765,7 @@ router.post("/api/telegram", async (req, res) => {
|
|
|
2855
2765
|
const acUrl = resolveProjectAgentchattrUrl(cfg, project);
|
|
2856
2766
|
if (acUrl) {
|
|
2857
2767
|
const acPort = new URL(acUrl).port || "8300";
|
|
2858
|
-
await fetch(`http://127.0.0.1:${acPort}/api/deregister/
|
|
2768
|
+
await fetch(`http://127.0.0.1:${acPort}/api/deregister/tg`, {
|
|
2859
2769
|
method: "POST",
|
|
2860
2770
|
signal: AbortSignal.timeout(3000),
|
|
2861
2771
|
}).catch(() => {});
|
|
@@ -2938,7 +2848,7 @@ const DISCORD_BRIDGE_SRC = path.join(__dirname, "..", "bridges", "discord");
|
|
|
2938
2848
|
const DISCORD_BRIDGE_DIR = path.join(CONFIG_DIR, "agentchattr-discord");
|
|
2939
2849
|
|
|
2940
2850
|
function discordPidFile(projectId) {
|
|
2941
|
-
return path.join(CONFIG_DIR, `
|
|
2851
|
+
return path.join(CONFIG_DIR, `dc-bridge-${projectId}.pid`);
|
|
2942
2852
|
}
|
|
2943
2853
|
|
|
2944
2854
|
function discordConfigToml(projectId) {
|
|
@@ -2946,11 +2856,16 @@ function discordConfigToml(projectId) {
|
|
|
2946
2856
|
}
|
|
2947
2857
|
|
|
2948
2858
|
function discordBridgeLog(projectId) {
|
|
2949
|
-
return path.join(CONFIG_DIR, `
|
|
2859
|
+
return path.join(CONFIG_DIR, `dc-bridge-${projectId}.log`);
|
|
2950
2860
|
}
|
|
2951
2861
|
|
|
2952
2862
|
function buildDiscordBridgeToml(dc, projectId) {
|
|
2953
|
-
const cursorFile = path.join(CONFIG_DIR, `
|
|
2863
|
+
const cursorFile = path.join(CONFIG_DIR, `dc-bridge-cursor-${projectId}.json`);
|
|
2864
|
+
// #439: migrate old cursor file so the bridge doesn't replay history
|
|
2865
|
+
const oldCursor = path.join(CONFIG_DIR, `discord-bridge-cursor-${projectId}.json`);
|
|
2866
|
+
if (!fs.existsSync(cursorFile) && fs.existsSync(oldCursor)) {
|
|
2867
|
+
fs.renameSync(oldCursor, cursorFile);
|
|
2868
|
+
}
|
|
2954
2869
|
return (
|
|
2955
2870
|
`[discord]\n` +
|
|
2956
2871
|
`bot_token = "${dc.bot_token}"\n` +
|
|
@@ -2961,11 +2876,14 @@ function buildDiscordBridgeToml(dc, projectId) {
|
|
|
2961
2876
|
}
|
|
2962
2877
|
|
|
2963
2878
|
function patchAgentchattrConfigForDiscordBridge(tomlText) {
|
|
2964
|
-
if
|
|
2965
|
-
|
|
2879
|
+
// #439: migrate old slug if present
|
|
2880
|
+
const original = tomlText;
|
|
2881
|
+
tomlText = tomlText.replace(/^\[agents\.discord-bridge\]\s*$/m, "[agents.dc]");
|
|
2882
|
+
if (/^\[agents\.dc\]\s*$/m.test(tomlText)) {
|
|
2883
|
+
return { text: tomlText, changed: tomlText !== original };
|
|
2966
2884
|
}
|
|
2967
2885
|
const sep = tomlText.length === 0 || tomlText.endsWith("\n") ? "" : "\n";
|
|
2968
|
-
const block = `\n[agents.
|
|
2886
|
+
const block = `\n[agents.dc]\nlabel = "Discord Bridge"\n`;
|
|
2969
2887
|
return { text: tomlText + sep + block, changed: true };
|
|
2970
2888
|
}
|
|
2971
2889
|
|
|
@@ -3147,10 +3065,12 @@ router.post("/api/discord", async (req, res) => {
|
|
|
3147
3065
|
`pip output tail:\n${pipOutput.split("\n").slice(-10).join("\n")}`,
|
|
3148
3066
|
});
|
|
3149
3067
|
}
|
|
3150
|
-
// Patch all project AC configs with [agents.
|
|
3068
|
+
// #457: Patch all project AC configs with [agents.dc] and
|
|
3069
|
+
// migrate old `discord-bridge` slug. Restart AC for changed projects.
|
|
3151
3070
|
const patched = [];
|
|
3152
3071
|
try {
|
|
3153
3072
|
const cfgAll = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
3073
|
+
const serverPort = cfgAll.port || 8400;
|
|
3154
3074
|
for (const proj of cfgAll.projects || []) {
|
|
3155
3075
|
if (!proj || !proj.id) continue;
|
|
3156
3076
|
const acPath = projectAgentchattrConfigPath(proj.id);
|
|
@@ -3161,6 +3081,14 @@ router.post("/api/discord", async (req, res) => {
|
|
|
3161
3081
|
if (changed) {
|
|
3162
3082
|
fs.writeFileSync(acPath, text);
|
|
3163
3083
|
patched.push(proj.id);
|
|
3084
|
+
// #457: restart AC so it loads the new agent slug
|
|
3085
|
+
setTimeout(async () => {
|
|
3086
|
+
try {
|
|
3087
|
+
await fetch(`http://127.0.0.1:${serverPort}/api/agentchattr/${encodeURIComponent(proj.id)}/restart`, {
|
|
3088
|
+
method: "POST",
|
|
3089
|
+
});
|
|
3090
|
+
} catch {}
|
|
3091
|
+
}, 1000);
|
|
3164
3092
|
}
|
|
3165
3093
|
} catch {}
|
|
3166
3094
|
}
|
|
@@ -3246,7 +3174,7 @@ router.post("/api/discord", async (req, res) => {
|
|
|
3246
3174
|
const acUrl = resolveProjectAgentchattrUrl(cfg, project);
|
|
3247
3175
|
if (acUrl) {
|
|
3248
3176
|
const acPort = new URL(acUrl).port || "8300";
|
|
3249
|
-
await fetch(`http://127.0.0.1:${acPort}/api/deregister/
|
|
3177
|
+
await fetch(`http://127.0.0.1:${acPort}/api/deregister/dc`, {
|
|
3250
3178
|
method: "POST",
|
|
3251
3179
|
signal: AbortSignal.timeout(3000),
|
|
3252
3180
|
}).catch(() => {});
|
|
@@ -3374,7 +3302,7 @@ module.exports.parseActiveBatch = parseActiveBatch;
|
|
|
3374
3302
|
// summarizeItems for the batch-progress fixture test.
|
|
3375
3303
|
module.exports.buildNoPrRow = buildNoPrRow;
|
|
3376
3304
|
module.exports.summarizeItems = summarizeItems;
|
|
3377
|
-
// #353: expose readLastLines for the
|
|
3305
|
+
// #353: expose readLastLines for the tg-bridge test.
|
|
3378
3306
|
module.exports.readLastLines = readLastLines;
|
|
3379
3307
|
// #380: expose checkTelegramBridgePythonDeps so the bridge test can
|
|
3380
3308
|
// exercise the venv-path interpreter argument round trip.
|
|
@@ -3390,6 +3318,7 @@ module.exports.checkDiscordBridgePythonDeps = checkDiscordBridgePythonDeps;
|
|
|
3390
3318
|
module.exports.buildDiscordBridgeToml = buildDiscordBridgeToml;
|
|
3391
3319
|
module.exports.patchAgentchattrConfigForDiscordBridge = patchAgentchattrConfigForDiscordBridge;
|
|
3392
3320
|
module.exports.buildDiscordBridgeSpawnEnv = buildDiscordBridgeSpawnEnv;
|
|
3321
|
+
module.exports.projectAgentchattrConfigPath = projectAgentchattrConfigPath;
|
|
3393
3322
|
// #236: expose sendViaWebSocket so the chat-ws-send regression test
|
|
3394
3323
|
// can verify the ack/body/error paths against a fake AC ws server.
|
|
3395
3324
|
module.exports.sendViaWebSocket = sendViaWebSocket;
|
|
@@ -183,7 +183,7 @@ try {
|
|
|
183
183
|
assert.match(toml13, /agentchattr_url = "http:\/\/127\.0\.0\.1:8301"/);
|
|
184
184
|
// #404: cursor_file must be per-project so multiple bridges
|
|
185
185
|
// don't clobber each other's position.
|
|
186
|
-
assert.match(toml13, /cursor_file = ".*
|
|
186
|
+
assert.match(toml13, /cursor_file = ".*tg-bridge-cursor-testproject\.json"/);
|
|
187
187
|
// Must NOT emit a separate [agentchattr] section — the bridge
|
|
188
188
|
// would silently ignore it.
|
|
189
189
|
assert.equal(toml13.includes("\n[agentchattr]\n"), false);
|
|
@@ -196,19 +196,19 @@ try {
|
|
|
196
196
|
"[agents.head]\nlabel = \"Head\"\n\n[agents.dev]\nlabel = \"Dev\"\n";
|
|
197
197
|
const first = patchAgentchattrConfigForTelegramBridge(baseConfig);
|
|
198
198
|
assert.equal(first.changed, true);
|
|
199
|
-
assert.match(first.text, /^\[agents\.
|
|
199
|
+
assert.match(first.text, /^\[agents\.tg\]$/m);
|
|
200
200
|
assert.match(first.text, /label = "Telegram Bridge"/);
|
|
201
201
|
// Running a second time is a no-op.
|
|
202
202
|
const second = patchAgentchattrConfigForTelegramBridge(first.text);
|
|
203
203
|
assert.equal(second.changed, false);
|
|
204
204
|
assert.equal(second.text, first.text);
|
|
205
|
-
//
|
|
206
|
-
//
|
|
205
|
+
// #439: a config with old slug [agents.telegram-bridge] is migrated
|
|
206
|
+
// to [agents.tg] and flagged as changed.
|
|
207
207
|
const handPatched =
|
|
208
208
|
baseConfig + "\n[agents.telegram-bridge]\nlabel = \"Telegram Bridge\"\n";
|
|
209
209
|
const third = patchAgentchattrConfigForTelegramBridge(handPatched);
|
|
210
|
-
assert.equal(third.changed,
|
|
211
|
-
assert.
|
|
210
|
+
assert.equal(third.changed, true);
|
|
211
|
+
assert.match(third.text, /^\[agents\.tg\]$/m);
|
|
212
212
|
|
|
213
213
|
// 15) #383 Bug 4: buildTelegramBridgeSpawnEnv strips the three
|
|
214
214
|
// env vars the upstream bridge treats as higher-precedence
|
package/templates/config.toml
CHANGED
|
@@ -38,15 +38,15 @@ color = "#da7756"
|
|
|
38
38
|
label = "Builder"
|
|
39
39
|
|
|
40
40
|
# #383: AC's registry rejects bases not declared in config.toml.
|
|
41
|
-
# The Telegram bridge registers as `
|
|
42
|
-
# per-project AC config must declare
|
|
43
|
-
# command/cwd of its own — it is a long-running
|
|
44
|
-
# that posts to AC's HTTP API.
|
|
45
|
-
[agents.
|
|
41
|
+
# The Telegram bridge registers as `tg` (#439: renamed from
|
|
42
|
+
# `telegram-bridge`), so every per-project AC config must declare
|
|
43
|
+
# it. The bridge has no command/cwd of its own — it is a long-running
|
|
44
|
+
# external client that posts to AC's HTTP API.
|
|
45
|
+
[agents.tg]
|
|
46
46
|
label = "Telegram Bridge"
|
|
47
47
|
|
|
48
|
-
# #399: Discord bridge registers as `discord-bridge
|
|
49
|
-
[agents.
|
|
48
|
+
# #399/#439: Discord bridge registers as `dc` (renamed from `discord-bridge`).
|
|
49
|
+
[agents.dc]
|
|
50
50
|
label = "Discord Bridge"
|
|
51
51
|
|
|
52
52
|
[routing]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
self.__SSG_MANIFEST=new Set(["\u002Fproject\u002F[id]","\u002Fproject\u002F[id]\u002Fmemory","\u002Fproject\u002F[id]\u002Fqueue"]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|