quadwork 0.1.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/LICENSE +21 -0
- package/README.md +135 -0
- package/bin/quadwork.js +686 -0
- package/out/404.html +1 -0
- package/out/__next.__PAGE__.txt +6 -0
- package/out/__next._full.txt +17 -0
- package/out/__next._head.txt +6 -0
- package/out/__next._index.txt +6 -0
- package/out/__next._tree.txt +3 -0
- package/out/_next/static/chunks/0.57eg262w~qg.js +1 -0
- package/out/_next/static/chunks/0.dzh0qf9zq1l.js +2 -0
- package/out/_next/static/chunks/03hi.hdp6l230.js +20 -0
- package/out/_next/static/chunks/03v5eoc-wic6o.js +1 -0
- package/out/_next/static/chunks/03yov._jigv17.js +1 -0
- package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
- package/out/_next/static/chunks/08fgie1bcjynm.js +1 -0
- package/out/_next/static/chunks/0excsn2a_5qsb.js +4 -0
- package/out/_next/static/chunks/0iqqouh_3i5y5.js +13 -0
- package/out/_next/static/chunks/0jsosmtclw5n5.js +4 -0
- package/out/_next/static/chunks/0ox7p_szjhn69.js +1 -0
- package/out/_next/static/chunks/0r7t_sj_sejq9.js +1 -0
- package/out/_next/static/chunks/13uu.sohs74zg.js +31 -0
- package/out/_next/static/chunks/15kwal..m9r49.css +2 -0
- package/out/_next/static/chunks/17oc2l.ekcs8b.css +1 -0
- package/out/_next/static/chunks/17sk4qv6_d0co.js +1 -0
- package/out/_next/static/chunks/turbopack-06pqx~0d8czn_.js +1 -0
- package/out/_next/static/eq3ebKZWXVJquNrlYMOZR/_buildManifest.js +15 -0
- package/out/_next/static/eq3ebKZWXVJquNrlYMOZR/_clientMiddlewareManifest.js +1 -0
- package/out/_next/static/eq3ebKZWXVJquNrlYMOZR/_ssgManifest.js +1 -0
- package/out/_next/static/media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 +0 -0
- package/out/_next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2 +0 -0
- package/out/_next/static/media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 +0 -0
- package/out/_next/static/media/favicon.0x3dzn~oxb6tn.ico +0 -0
- package/out/_not-found/__next._full.txt +17 -0
- package/out/_not-found/__next._head.txt +6 -0
- package/out/_not-found/__next._index.txt +6 -0
- package/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
- package/out/_not-found/__next._not-found.txt +5 -0
- package/out/_not-found/__next._tree.txt +2 -0
- package/out/_not-found.html +1 -0
- package/out/_not-found.txt +17 -0
- package/out/favicon.ico +0 -0
- package/out/file.svg +1 -0
- package/out/globe.svg +1 -0
- package/out/index.html +1 -0
- package/out/index.txt +17 -0
- package/out/next.svg +1 -0
- package/out/project/_/__next._full.txt +20 -0
- package/out/project/_/__next._head.txt +6 -0
- package/out/project/_/__next._index.txt +6 -0
- package/out/project/_/__next._tree.txt +4 -0
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +7 -0
- package/out/project/_/__next.project.$d$id.txt +5 -0
- package/out/project/_/__next.project.txt +5 -0
- package/out/project/_/memory/__next._full.txt +19 -0
- package/out/project/_/memory/__next._head.txt +6 -0
- package/out/project/_/memory/__next._index.txt +6 -0
- package/out/project/_/memory/__next._tree.txt +3 -0
- package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +6 -0
- package/out/project/_/memory/__next.project.$d$id.memory.txt +5 -0
- package/out/project/_/memory/__next.project.$d$id.txt +5 -0
- package/out/project/_/memory/__next.project.txt +5 -0
- package/out/project/_/memory.html +1 -0
- package/out/project/_/memory.txt +19 -0
- package/out/project/_/queue/__next._full.txt +19 -0
- package/out/project/_/queue/__next._head.txt +6 -0
- package/out/project/_/queue/__next._index.txt +6 -0
- package/out/project/_/queue/__next._tree.txt +3 -0
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +6 -0
- package/out/project/_/queue/__next.project.$d$id.queue.txt +5 -0
- package/out/project/_/queue/__next.project.$d$id.txt +5 -0
- package/out/project/_/queue/__next.project.txt +5 -0
- package/out/project/_/queue.html +1 -0
- package/out/project/_/queue.txt +19 -0
- package/out/project/_.html +1 -0
- package/out/project/_.txt +20 -0
- package/out/settings/__next._full.txt +19 -0
- package/out/settings/__next._head.txt +6 -0
- package/out/settings/__next._index.txt +6 -0
- package/out/settings/__next._tree.txt +3 -0
- package/out/settings/__next.settings.__PAGE__.txt +6 -0
- package/out/settings/__next.settings.txt +5 -0
- package/out/settings.html +1 -0
- package/out/settings.txt +19 -0
- package/out/setup/__next._full.txt +19 -0
- package/out/setup/__next._head.txt +6 -0
- package/out/setup/__next._index.txt +6 -0
- package/out/setup/__next._tree.txt +3 -0
- package/out/setup/__next.setup.__PAGE__.txt +6 -0
- package/out/setup/__next.setup.txt +5 -0
- package/out/setup.html +1 -0
- package/out/setup.txt +19 -0
- package/out/vercel.svg +1 -0
- package/out/window.svg +1 -0
- package/package.json +61 -0
- package/server/config.js +63 -0
- package/server/index.js +476 -0
- package/server/routes.js +889 -0
- package/templates/CLAUDE.md +57 -0
- package/templates/config.toml +46 -0
- package/templates/seeds/t1.AGENTS.md +55 -0
- package/templates/seeds/t2a.AGENTS.md +96 -0
- package/templates/seeds/t2b.AGENTS.md +96 -0
- package/templates/seeds/t3.AGENTS.md +80 -0
- package/templates/wrapper.py +70 -0
package/bin/quadwork.js
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const readline = require("readline");
|
|
8
|
+
|
|
9
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), ".quadwork");
|
|
12
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
13
|
+
const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
|
|
14
|
+
const AGENTS = ["t1", "t2a", "t2b", "t3"];
|
|
15
|
+
|
|
16
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function log(msg) { console.log(` ${msg}`); }
|
|
19
|
+
function ok(msg) { console.log(` ✓ ${msg}`); }
|
|
20
|
+
function warn(msg) { console.log(` ⚠ ${msg}`); }
|
|
21
|
+
function fail(msg) { console.error(` ✗ ${msg}`); }
|
|
22
|
+
function header(msg) { console.log(`\n── ${msg} ${"─".repeat(Math.max(0, 58 - msg.length))}\n`); }
|
|
23
|
+
|
|
24
|
+
function run(cmd, opts = {}) {
|
|
25
|
+
try {
|
|
26
|
+
return execSync(cmd, { encoding: "utf-8", stdio: "pipe", ...opts }).trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function which(cmd) {
|
|
33
|
+
return run(`which ${cmd}`) !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ask(rl, question, defaultVal) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const suffix = defaultVal ? ` (${defaultVal})` : "";
|
|
39
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
40
|
+
resolve(answer.trim() || defaultVal || "");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function askYN(rl, question, defaultYes = false) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
48
|
+
rl.question(` ${question} [${hint}]: `, (answer) => {
|
|
49
|
+
const a = answer.trim().toLowerCase();
|
|
50
|
+
resolve(a === "" ? defaultYes : a === "y" || a === "yes");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readConfig() {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return { port: 8400, agentchattr_url: "http://127.0.0.1:8300", projects: [] };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeConfig(config) {
|
|
64
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
65
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Prerequisites ──────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function checkPrereqs() {
|
|
71
|
+
header("Step 1: Prerequisites");
|
|
72
|
+
let allOk = true;
|
|
73
|
+
|
|
74
|
+
// Node.js 20+
|
|
75
|
+
const nodeVer = run("node --version");
|
|
76
|
+
if (nodeVer) {
|
|
77
|
+
const major = parseInt(nodeVer.replace("v", "").split(".")[0], 10);
|
|
78
|
+
if (major >= 20) ok(`Node.js ${nodeVer}`);
|
|
79
|
+
else { fail(`Node.js ${nodeVer} — need 20+`); allOk = false; }
|
|
80
|
+
} else { fail("Node.js not found"); allOk = false; }
|
|
81
|
+
|
|
82
|
+
// Python 3.10+
|
|
83
|
+
const pyVer = run("python3 --version");
|
|
84
|
+
if (pyVer) {
|
|
85
|
+
const parts = pyVer.replace("Python ", "").split(".");
|
|
86
|
+
const minor = parseInt(parts[1], 10);
|
|
87
|
+
if (parseInt(parts[0], 10) >= 3 && minor >= 10) ok(`${pyVer}`);
|
|
88
|
+
else { fail(`${pyVer} — need 3.10+`); allOk = false; }
|
|
89
|
+
} else { fail("Python 3 not found"); allOk = false; }
|
|
90
|
+
|
|
91
|
+
// AgentChattr
|
|
92
|
+
const acVer = run("agentchattr --version") || run("python3 -m agentchattr --version");
|
|
93
|
+
if (acVer) ok(`AgentChattr ${acVer}`);
|
|
94
|
+
else { warn("AgentChattr not found — install: pip install agentchattr"); allOk = false; }
|
|
95
|
+
|
|
96
|
+
// gh CLI
|
|
97
|
+
if (which("gh")) ok("GitHub CLI (gh)");
|
|
98
|
+
else { fail("GitHub CLI not found — install: https://cli.github.com"); allOk = false; }
|
|
99
|
+
|
|
100
|
+
// Claude Code or Codex
|
|
101
|
+
const hasClaude = which("claude");
|
|
102
|
+
const hasCodex = which("codex");
|
|
103
|
+
if (hasClaude) ok("Claude Code");
|
|
104
|
+
if (hasCodex) ok("Codex CLI");
|
|
105
|
+
if (!hasClaude && !hasCodex) {
|
|
106
|
+
fail("No AI CLI found — install Claude Code or Codex CLI");
|
|
107
|
+
allOk = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return allOk;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── GitHub ─────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async function setupGitHub(rl) {
|
|
116
|
+
header("Step 2: GitHub Connection");
|
|
117
|
+
|
|
118
|
+
// Check auth
|
|
119
|
+
const authStatus = run("gh auth status 2>&1");
|
|
120
|
+
if (authStatus && authStatus.includes("Logged in")) {
|
|
121
|
+
ok("GitHub authenticated");
|
|
122
|
+
} else {
|
|
123
|
+
fail("Not authenticated with GitHub — run: gh auth login");
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const repo = await ask(rl, "GitHub repo (owner/repo)", "");
|
|
128
|
+
if (!repo || !repo.includes("/")) {
|
|
129
|
+
fail("Invalid repo format — use owner/repo");
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Verify repo exists
|
|
134
|
+
const repoCheck = run(`gh repo view ${repo} --json name 2>&1`);
|
|
135
|
+
if (repoCheck && repoCheck.includes('"name"')) {
|
|
136
|
+
ok(`Repo ${repo} verified`);
|
|
137
|
+
} else {
|
|
138
|
+
fail(`Cannot access ${repo} — check permissions`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return repo;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Agent Configuration ────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
async function setupAgents(rl, repo) {
|
|
148
|
+
header("Step 3: Agent Configuration");
|
|
149
|
+
|
|
150
|
+
// Prompt for CLI backend
|
|
151
|
+
const hasClaude = which("claude");
|
|
152
|
+
const hasCodex = which("codex");
|
|
153
|
+
let defaultBackend = hasClaude ? "claude" : "codex";
|
|
154
|
+
const backend = await ask(rl, "CLI backend for agents (claude/codex)", defaultBackend);
|
|
155
|
+
if (backend !== "claude" && backend !== "codex") {
|
|
156
|
+
fail("Backend must be 'claude' or 'codex'");
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const projectDir = await ask(rl, "Project directory", process.cwd());
|
|
161
|
+
const absDir = path.resolve(projectDir);
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(absDir)) {
|
|
164
|
+
fail(`Directory not found: ${absDir}`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if it's a git repo
|
|
169
|
+
if (!fs.existsSync(path.join(absDir, ".git"))) {
|
|
170
|
+
fail(`Not a git repo: ${absDir}`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Prompt for reviewer credentials (used in T2a/T2b seed templates)
|
|
175
|
+
const reviewerUser = await ask(rl, "Reviewer GitHub username (for T2a/T2b)", "");
|
|
176
|
+
const reviewerTokenPath = await ask(rl, "Reviewer token file path (for T2a/T2b)", path.join(os.homedir(), ".quadwork", "reviewer-token"));
|
|
177
|
+
|
|
178
|
+
const projectName = path.basename(absDir);
|
|
179
|
+
log(`Project: ${projectName}`);
|
|
180
|
+
log("Creating worktrees for 4 agents...\n");
|
|
181
|
+
|
|
182
|
+
const worktrees = {};
|
|
183
|
+
for (const agent of AGENTS) {
|
|
184
|
+
const wtDir = path.join(path.dirname(absDir), `${projectName}-${agent}`);
|
|
185
|
+
if (fs.existsSync(wtDir)) {
|
|
186
|
+
ok(`Worktree exists: ${agent} → ${wtDir}`);
|
|
187
|
+
} else {
|
|
188
|
+
const branchName = `worktree-${agent}`;
|
|
189
|
+
// Create branch if needed
|
|
190
|
+
run(`git -C "${absDir}" branch ${branchName} HEAD 2>&1`);
|
|
191
|
+
const result = run(`git -C "${absDir}" worktree add "${wtDir}" ${branchName} 2>&1`);
|
|
192
|
+
if (result !== null) {
|
|
193
|
+
ok(`Created worktree: ${agent} → ${wtDir}`);
|
|
194
|
+
} else {
|
|
195
|
+
// Try without branch (detached)
|
|
196
|
+
const result2 = run(`git -C "${absDir}" worktree add --detach "${wtDir}" HEAD 2>&1`);
|
|
197
|
+
if (result2 !== null) ok(`Created worktree (detached): ${agent} → ${wtDir}`);
|
|
198
|
+
else { fail(`Failed to create worktree for ${agent}`); return null; }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
worktrees[agent] = wtDir;
|
|
202
|
+
|
|
203
|
+
// Copy AGENTS.md seed with placeholder substitution
|
|
204
|
+
const seedSrc = path.join(TEMPLATES_DIR, "seeds", `${agent}.AGENTS.md`);
|
|
205
|
+
const seedDst = path.join(wtDir, "AGENTS.md");
|
|
206
|
+
if (fs.existsSync(seedSrc)) {
|
|
207
|
+
let seedContent = fs.readFileSync(seedSrc, "utf-8");
|
|
208
|
+
seedContent = seedContent.replace(/\{\{reviewer_github_user\}\}/g, reviewerUser);
|
|
209
|
+
seedContent = seedContent.replace(/\{\{reviewer_token_path\}\}/g, reviewerTokenPath);
|
|
210
|
+
fs.writeFileSync(seedDst, seedContent);
|
|
211
|
+
log(` Copied ${agent}.AGENTS.md`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Copy CLAUDE.md to each worktree
|
|
216
|
+
const claudeSrc = path.join(TEMPLATES_DIR, "CLAUDE.md");
|
|
217
|
+
if (fs.existsSync(claudeSrc)) {
|
|
218
|
+
let claudeContent = fs.readFileSync(claudeSrc, "utf-8");
|
|
219
|
+
claudeContent = claudeContent.replace(/\{\{project_name\}\}/g, projectName);
|
|
220
|
+
for (const agent of AGENTS) {
|
|
221
|
+
const dst = path.join(worktrees[agent], "CLAUDE.md");
|
|
222
|
+
// Don't overwrite if CLAUDE.md already exists
|
|
223
|
+
if (!fs.existsSync(dst)) {
|
|
224
|
+
fs.writeFileSync(dst, claudeContent);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
ok("Copied CLAUDE.md to all worktrees");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { projectName, absDir, worktrees, repo, backend };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── AgentChattr Config ─────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function writeAgentChattrConfig(setup, configTomlPath) {
|
|
236
|
+
header("Step 4: AgentChattr Setup");
|
|
237
|
+
|
|
238
|
+
let tomlContent = fs.readFileSync(path.join(TEMPLATES_DIR, "config.toml"), "utf-8");
|
|
239
|
+
for (const agent of AGENTS) {
|
|
240
|
+
tomlContent = tomlContent.replace(`{{${agent}_cwd}}`, setup.worktrees[agent]);
|
|
241
|
+
}
|
|
242
|
+
// Replace placeholders
|
|
243
|
+
tomlContent = tomlContent.replace(/\{\{project_name\}\}/g, setup.projectName);
|
|
244
|
+
tomlContent = tomlContent.replace(/\{\{repo\}\}/g, setup.repo);
|
|
245
|
+
// Replace all agent commands with the chosen backend
|
|
246
|
+
tomlContent = tomlContent.replace(/command = "(?:claude|codex)"/g, `command = "${setup.backend}"`);
|
|
247
|
+
|
|
248
|
+
// Write config.toml
|
|
249
|
+
const configDir = path.dirname(configTomlPath);
|
|
250
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
251
|
+
fs.writeFileSync(configTomlPath, tomlContent);
|
|
252
|
+
ok(`Wrote ${configTomlPath}`);
|
|
253
|
+
|
|
254
|
+
// Install AgentChattr if missing, then start it
|
|
255
|
+
const acInstalled = run("agentchattr --version") || run("python3 -m agentchattr --version");
|
|
256
|
+
if (!acInstalled) {
|
|
257
|
+
log("Installing AgentChattr...");
|
|
258
|
+
const installResult = run("pip install agentchattr 2>&1");
|
|
259
|
+
if (installResult !== null) ok("Installed AgentChattr");
|
|
260
|
+
else warn("Failed to install AgentChattr — install manually: pip install agentchattr");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Start AgentChattr server
|
|
264
|
+
log("Starting AgentChattr server...");
|
|
265
|
+
const acProc = spawn("agentchattr", ["--config", configTomlPath], {
|
|
266
|
+
stdio: "ignore",
|
|
267
|
+
detached: true,
|
|
268
|
+
});
|
|
269
|
+
acProc.unref();
|
|
270
|
+
if (acProc.pid) {
|
|
271
|
+
ok(`AgentChattr started (PID: ${acProc.pid})`);
|
|
272
|
+
// Save PID for stop
|
|
273
|
+
const pidFile = path.join(CONFIG_DIR, "agentchattr.pid");
|
|
274
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
275
|
+
fs.writeFileSync(pidFile, String(acProc.pid));
|
|
276
|
+
} else {
|
|
277
|
+
warn("Could not start AgentChattr — start manually: agentchattr --config " + configTomlPath);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return configTomlPath;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Optional Add-ons ───────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
async function setupAddons(rl, setup, configTomlPath) {
|
|
286
|
+
header("Step 5: Optional Add-ons");
|
|
287
|
+
|
|
288
|
+
// Telegram Bridge
|
|
289
|
+
const wantTelegram = await askYN(rl, "Set up Telegram Bridge?", false);
|
|
290
|
+
if (wantTelegram) {
|
|
291
|
+
const telegramDir = path.join(path.dirname(setup.absDir), "agentchattr-telegram");
|
|
292
|
+
if (!fs.existsSync(telegramDir)) {
|
|
293
|
+
log("Cloning agentchattr-telegram...");
|
|
294
|
+
const cloneResult = run(`git clone https://github.com/realproject7/agentchattr-telegram.git "${telegramDir}" 2>&1`);
|
|
295
|
+
if (cloneResult !== null) ok("Cloned agentchattr-telegram");
|
|
296
|
+
else warn("Failed to clone — you can set it up manually later");
|
|
297
|
+
} else {
|
|
298
|
+
ok("agentchattr-telegram already present");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (fs.existsSync(telegramDir)) {
|
|
302
|
+
const reqFile = path.join(telegramDir, "requirements.txt");
|
|
303
|
+
if (fs.existsSync(reqFile)) {
|
|
304
|
+
run(`pip install -r "${reqFile}" 2>&1`);
|
|
305
|
+
ok("Installed Telegram Bridge dependencies");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const botToken = await ask(rl, "Telegram bot token", "");
|
|
309
|
+
const chatId = await ask(rl, "Telegram chat ID", "");
|
|
310
|
+
|
|
311
|
+
if (botToken && chatId) {
|
|
312
|
+
// Write bot token to ~/.quadwork/.env (never stored in config files)
|
|
313
|
+
const envPath = path.join(CONFIG_DIR, ".env");
|
|
314
|
+
const envKey = `TELEGRAM_BOT_TOKEN_${setup.projectName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
315
|
+
let envContent = "";
|
|
316
|
+
try { envContent = fs.readFileSync(envPath, "utf-8"); } catch {}
|
|
317
|
+
const envRegex = new RegExp(`^${envKey}=.*$`, "m");
|
|
318
|
+
const envLine = `${envKey}=${botToken}`;
|
|
319
|
+
if (envRegex.test(envContent)) {
|
|
320
|
+
envContent = envContent.replace(envRegex, envLine);
|
|
321
|
+
} else {
|
|
322
|
+
envContent = envContent.trimEnd() + (envContent ? "\n" : "") + envLine + "\n";
|
|
323
|
+
}
|
|
324
|
+
fs.writeFileSync(envPath, envContent, { mode: 0o600 });
|
|
325
|
+
fs.chmodSync(envPath, 0o600);
|
|
326
|
+
ok(`Saved bot token to ${envPath}`);
|
|
327
|
+
|
|
328
|
+
// Persist telegram settings for writeQuadWorkConfig (env reference, not plaintext)
|
|
329
|
+
setup.telegram = {
|
|
330
|
+
bot_token: `env:${envKey}`,
|
|
331
|
+
chat_id: chatId,
|
|
332
|
+
bridge_dir: telegramDir,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Append telegram section to config.toml (token read from env at runtime)
|
|
336
|
+
const telegramSection = `
|
|
337
|
+
[telegram]
|
|
338
|
+
bot_token = "env:${envKey}"
|
|
339
|
+
chat_id = "${chatId}"
|
|
340
|
+
agentchattr_url = "http://127.0.0.1:8300"
|
|
341
|
+
poll_interval = 2
|
|
342
|
+
bridge_sender = "telegram-bridge"
|
|
343
|
+
`;
|
|
344
|
+
fs.appendFileSync(configTomlPath, telegramSection);
|
|
345
|
+
ok("Added Telegram config to config.toml (token stored in .env)");
|
|
346
|
+
|
|
347
|
+
// Start Telegram bridge daemon with a resolved config (real token, chmod 600)
|
|
348
|
+
const bridgeScript = path.join(telegramDir, "telegram_bridge.py");
|
|
349
|
+
if (fs.existsSync(bridgeScript)) {
|
|
350
|
+
log("Starting Telegram bridge...");
|
|
351
|
+
const bridgeToml = path.join(CONFIG_DIR, `telegram-${setup.projectName}.toml`);
|
|
352
|
+
const bridgeTomlContent = `[telegram]\nbot_token = "${botToken}"\nchat_id = "${chatId}"\n\n[agentchattr]\nurl = "http://127.0.0.1:8300"\n`;
|
|
353
|
+
fs.writeFileSync(bridgeToml, bridgeTomlContent, { mode: 0o600 });
|
|
354
|
+
fs.chmodSync(bridgeToml, 0o600);
|
|
355
|
+
const bridgeProc = spawn("python3", [bridgeScript, "--config", bridgeToml], {
|
|
356
|
+
stdio: "ignore",
|
|
357
|
+
detached: true,
|
|
358
|
+
});
|
|
359
|
+
bridgeProc.unref();
|
|
360
|
+
if (bridgeProc.pid) {
|
|
361
|
+
ok(`Telegram bridge started (PID: ${bridgeProc.pid})`);
|
|
362
|
+
const pidFile = path.join(CONFIG_DIR, "telegram-bridge.pid");
|
|
363
|
+
fs.writeFileSync(pidFile, String(bridgeProc.pid));
|
|
364
|
+
} else {
|
|
365
|
+
warn("Could not start Telegram bridge — start manually");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Shared Memory
|
|
373
|
+
const wantMemory = await askYN(rl, "Set up Shared Memory?", false);
|
|
374
|
+
if (wantMemory) {
|
|
375
|
+
const memoryDir = path.join(path.dirname(setup.absDir), "agent-memory");
|
|
376
|
+
if (!fs.existsSync(memoryDir)) {
|
|
377
|
+
log("Cloning agent-memory...");
|
|
378
|
+
const cloneResult = run(`git clone https://github.com/realproject7/agent-memory.git "${memoryDir}" 2>&1`);
|
|
379
|
+
if (cloneResult !== null) ok("Cloned agent-memory");
|
|
380
|
+
else warn("Failed to clone — you can set it up manually later");
|
|
381
|
+
} else {
|
|
382
|
+
ok("agent-memory already present");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (fs.existsSync(memoryDir)) {
|
|
386
|
+
// Verify butler scripts exist
|
|
387
|
+
const scriptsDir = path.join(memoryDir, "scripts");
|
|
388
|
+
const requiredScripts = ["butler-scan.sh", "butler-consolidate.sh", "inject.sh"];
|
|
389
|
+
for (const script of requiredScripts) {
|
|
390
|
+
const scriptPath = path.join(scriptsDir, script);
|
|
391
|
+
if (fs.existsSync(scriptPath)) {
|
|
392
|
+
// Ensure executable
|
|
393
|
+
try { fs.chmodSync(scriptPath, 0o755); } catch {}
|
|
394
|
+
} else {
|
|
395
|
+
warn(`Butler script not found: ${scriptPath}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
ok("Butler scripts verified");
|
|
399
|
+
|
|
400
|
+
// Create project short-term memory file if missing
|
|
401
|
+
const shortTermDir = path.join(memoryDir, "central", "short-term");
|
|
402
|
+
const projectMemFile = path.join(shortTermDir, `${setup.projectName}.md`);
|
|
403
|
+
if (!fs.existsSync(projectMemFile)) {
|
|
404
|
+
if (!fs.existsSync(shortTermDir)) fs.mkdirSync(shortTermDir, { recursive: true });
|
|
405
|
+
fs.writeFileSync(projectMemFile, `# ${setup.projectName} — Short-Term Memory\n\n_No entries yet._\n`);
|
|
406
|
+
ok(`Created ${projectMemFile}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Create cards directory if missing
|
|
410
|
+
const cardsDir = path.join(memoryDir, "archive", "v2", "cards");
|
|
411
|
+
if (!fs.existsSync(cardsDir)) {
|
|
412
|
+
fs.mkdirSync(cardsDir, { recursive: true });
|
|
413
|
+
ok("Created cards directory");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
setup.memoryDir = memoryDir;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return setup;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ─── Write QuadWork Config ──────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
function writeQuadWorkConfig(setup) {
|
|
426
|
+
header("Writing QuadWork Config");
|
|
427
|
+
|
|
428
|
+
const config = readConfig();
|
|
429
|
+
|
|
430
|
+
const project = {
|
|
431
|
+
id: setup.projectName,
|
|
432
|
+
name: setup.projectName,
|
|
433
|
+
repo: setup.repo,
|
|
434
|
+
working_dir: setup.absDir,
|
|
435
|
+
agents: {},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
for (const agent of AGENTS) {
|
|
439
|
+
project.agents[agent] = { cwd: setup.worktrees[agent], command: setup.backend };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (setup.memoryDir) {
|
|
443
|
+
project.memory_cards_dir = path.join(setup.memoryDir, "archive", "v2", "cards");
|
|
444
|
+
project.shared_memory_path = path.join(setup.memoryDir, "central", "short-term", `${setup.projectName}.md`);
|
|
445
|
+
project.butler_scripts_dir = path.join(setup.memoryDir, "scripts");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (setup.telegram) {
|
|
449
|
+
project.telegram = {
|
|
450
|
+
bot_token: setup.telegram.bot_token,
|
|
451
|
+
chat_id: setup.telegram.chat_id,
|
|
452
|
+
bridge_dir: setup.telegram.bridge_dir,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Upsert project
|
|
457
|
+
const idx = config.projects.findIndex((p) => p.id === setup.projectName);
|
|
458
|
+
if (idx >= 0) config.projects[idx] = project;
|
|
459
|
+
else config.projects.push(project);
|
|
460
|
+
|
|
461
|
+
writeConfig(config);
|
|
462
|
+
ok(`Wrote ${CONFIG_PATH}`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ─── Init Command ───────────────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
async function cmdInit() {
|
|
468
|
+
console.log("\n QuadWork Init — 4-agent coding team setup\n");
|
|
469
|
+
|
|
470
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
// Step 1: Prerequisites
|
|
474
|
+
const prereqsOk = checkPrereqs();
|
|
475
|
+
if (!prereqsOk) {
|
|
476
|
+
const proceed = await askYN(rl, "Some prerequisites missing. Continue anyway?", false);
|
|
477
|
+
if (!proceed) { rl.close(); process.exit(1); }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Step 2: GitHub
|
|
481
|
+
const repo = await setupGitHub(rl);
|
|
482
|
+
if (!repo) { rl.close(); process.exit(1); }
|
|
483
|
+
|
|
484
|
+
// Step 3: Agents
|
|
485
|
+
const setup = await setupAgents(rl, repo);
|
|
486
|
+
if (!setup) { rl.close(); process.exit(1); }
|
|
487
|
+
|
|
488
|
+
// Step 4: AgentChattr config
|
|
489
|
+
const configTomlPath = path.join(setup.absDir, "config.toml");
|
|
490
|
+
writeAgentChattrConfig(setup, configTomlPath);
|
|
491
|
+
|
|
492
|
+
// Step 5: Optional add-ons
|
|
493
|
+
await setupAddons(rl, setup, configTomlPath);
|
|
494
|
+
|
|
495
|
+
// Write QuadWork config
|
|
496
|
+
writeQuadWorkConfig(setup);
|
|
497
|
+
|
|
498
|
+
// Done
|
|
499
|
+
header("Setup Complete");
|
|
500
|
+
log(`Project: ${setup.projectName}`);
|
|
501
|
+
log(`Repo: ${setup.repo}`);
|
|
502
|
+
log(`Worktrees: ${AGENTS.map((a) => `${a}/`).join(", ")}`);
|
|
503
|
+
log(`Config: ${CONFIG_PATH}`);
|
|
504
|
+
log(`AgentChattr: ${configTomlPath}`);
|
|
505
|
+
log("");
|
|
506
|
+
log("Next steps:");
|
|
507
|
+
log(" npx quadwork start — launch dashboard + agents");
|
|
508
|
+
log(" npx quadwork stop — stop all processes");
|
|
509
|
+
log("");
|
|
510
|
+
|
|
511
|
+
rl.close();
|
|
512
|
+
} catch (err) {
|
|
513
|
+
fail(err.message);
|
|
514
|
+
rl.close();
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ─── Start Command ──────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
function cmdStart() {
|
|
522
|
+
console.log("\n QuadWork Start\n");
|
|
523
|
+
|
|
524
|
+
const config = readConfig();
|
|
525
|
+
if (config.projects.length === 0) {
|
|
526
|
+
fail("No projects configured. Run: npx quadwork init");
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const quadworkDir = path.join(__dirname, "..");
|
|
531
|
+
const port = config.port || 8400;
|
|
532
|
+
|
|
533
|
+
// Check that the pre-built frontend exists
|
|
534
|
+
const outDir = path.join(quadworkDir, "out");
|
|
535
|
+
if (!fs.existsSync(outDir)) {
|
|
536
|
+
warn("Frontend not found (out/ missing). API will work but UI won't load.");
|
|
537
|
+
warn("If running from source, run: npm run build");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Start single Express server (serves API + WebSocket + static frontend)
|
|
541
|
+
const serverDir = path.join(quadworkDir, "server");
|
|
542
|
+
if (!fs.existsSync(path.join(serverDir, "index.js"))) {
|
|
543
|
+
fail("Server not found. Run from the quadwork directory.");
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
log("Starting QuadWork server...");
|
|
548
|
+
const server = spawn("node", [serverDir], {
|
|
549
|
+
stdio: "ignore",
|
|
550
|
+
detached: true,
|
|
551
|
+
env: { ...process.env },
|
|
552
|
+
});
|
|
553
|
+
server.unref();
|
|
554
|
+
ok(`Server started (PID: ${server.pid})`);
|
|
555
|
+
|
|
556
|
+
// Save PID for stop command
|
|
557
|
+
const pidFile = path.join(CONFIG_DIR, "server.pid");
|
|
558
|
+
fs.writeFileSync(pidFile, String(server.pid));
|
|
559
|
+
|
|
560
|
+
// Start AgentChattr if config.toml exists for first project
|
|
561
|
+
const firstProject = config.projects[0];
|
|
562
|
+
if (firstProject) {
|
|
563
|
+
const configToml = path.join(firstProject.working_dir, "config.toml");
|
|
564
|
+
if (fs.existsSync(configToml)) {
|
|
565
|
+
const acProc = spawn("agentchattr", ["--config", configToml], {
|
|
566
|
+
stdio: "ignore",
|
|
567
|
+
detached: true,
|
|
568
|
+
});
|
|
569
|
+
acProc.unref();
|
|
570
|
+
if (acProc.pid) {
|
|
571
|
+
ok(`AgentChattr started (PID: ${acProc.pid})`);
|
|
572
|
+
fs.writeFileSync(path.join(CONFIG_DIR, "agentchattr.pid"), String(acProc.pid));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Open dashboard in browser
|
|
578
|
+
const dashboardUrl = `http://127.0.0.1:${port}`;
|
|
579
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
try { execSync(`${openCmd} ${dashboardUrl}`, { stdio: "ignore" }); } catch {}
|
|
582
|
+
}, 1500);
|
|
583
|
+
|
|
584
|
+
log(`Dashboard: ${dashboardUrl}`);
|
|
585
|
+
log("");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ─── Stop Command ───────────────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
function stopPid(name, pidFileName) {
|
|
591
|
+
const pidFile = path.join(CONFIG_DIR, pidFileName);
|
|
592
|
+
if (fs.existsSync(pidFile)) {
|
|
593
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
594
|
+
try {
|
|
595
|
+
process.kill(pid, "SIGTERM");
|
|
596
|
+
ok(`Stopped ${name} (PID: ${pid})`);
|
|
597
|
+
} catch {
|
|
598
|
+
warn(`${name} process ${pid} not running`);
|
|
599
|
+
}
|
|
600
|
+
fs.unlinkSync(pidFile);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function cmdStop() {
|
|
607
|
+
console.log("\n QuadWork Stop\n");
|
|
608
|
+
|
|
609
|
+
let stopped = 0;
|
|
610
|
+
if (stopPid("Telegram bridge", "telegram-bridge.pid")) stopped++;
|
|
611
|
+
if (stopPid("AgentChattr", "agentchattr.pid")) stopped++;
|
|
612
|
+
if (stopPid("Server", "server.pid")) stopped++;
|
|
613
|
+
|
|
614
|
+
if (stopped === 0) warn("No running processes found");
|
|
615
|
+
else ok(`Stopped ${stopped} process(es)`);
|
|
616
|
+
log("");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ─── Add Project Command ────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
async function cmdAddProject() {
|
|
622
|
+
console.log("\n QuadWork — Add Project\n");
|
|
623
|
+
|
|
624
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
const repo = await setupGitHub(rl);
|
|
628
|
+
if (!repo) { rl.close(); process.exit(1); }
|
|
629
|
+
|
|
630
|
+
const setup = await setupAgents(rl, repo);
|
|
631
|
+
if (!setup) { rl.close(); process.exit(1); }
|
|
632
|
+
|
|
633
|
+
const configTomlPath = path.join(setup.absDir, "config.toml");
|
|
634
|
+
writeAgentChattrConfig(setup, configTomlPath);
|
|
635
|
+
|
|
636
|
+
writeQuadWorkConfig(setup);
|
|
637
|
+
|
|
638
|
+
header("Project Added");
|
|
639
|
+
log(`Project: ${setup.projectName}`);
|
|
640
|
+
log(`Repo: ${setup.repo}`);
|
|
641
|
+
log(`Worktrees: ${AGENTS.map((a) => `${a}/`).join(", ")}`);
|
|
642
|
+
log("");
|
|
643
|
+
|
|
644
|
+
rl.close();
|
|
645
|
+
} catch (err) {
|
|
646
|
+
fail(err.message);
|
|
647
|
+
rl.close();
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
const command = process.argv[2];
|
|
655
|
+
|
|
656
|
+
switch (command) {
|
|
657
|
+
case "init":
|
|
658
|
+
cmdInit();
|
|
659
|
+
break;
|
|
660
|
+
case "start":
|
|
661
|
+
cmdStart();
|
|
662
|
+
break;
|
|
663
|
+
case "stop":
|
|
664
|
+
cmdStop();
|
|
665
|
+
break;
|
|
666
|
+
case "add-project":
|
|
667
|
+
cmdAddProject();
|
|
668
|
+
break;
|
|
669
|
+
default:
|
|
670
|
+
console.log(`
|
|
671
|
+
Usage: quadwork <command>
|
|
672
|
+
|
|
673
|
+
Commands:
|
|
674
|
+
init Set up a new QuadWork 4-agent environment
|
|
675
|
+
start Start the QuadWork dashboard and backend
|
|
676
|
+
stop Stop all QuadWork processes
|
|
677
|
+
add-project Add a project to an existing QuadWork setup
|
|
678
|
+
|
|
679
|
+
Examples:
|
|
680
|
+
npx quadwork init
|
|
681
|
+
npx quadwork start
|
|
682
|
+
npx quadwork stop
|
|
683
|
+
npx quadwork add-project
|
|
684
|
+
`);
|
|
685
|
+
if (command) process.exit(1);
|
|
686
|
+
}
|