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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/bin/quadwork.js +686 -0
  4. package/out/404.html +1 -0
  5. package/out/__next.__PAGE__.txt +6 -0
  6. package/out/__next._full.txt +17 -0
  7. package/out/__next._head.txt +6 -0
  8. package/out/__next._index.txt +6 -0
  9. package/out/__next._tree.txt +3 -0
  10. package/out/_next/static/chunks/0.57eg262w~qg.js +1 -0
  11. package/out/_next/static/chunks/0.dzh0qf9zq1l.js +2 -0
  12. package/out/_next/static/chunks/03hi.hdp6l230.js +20 -0
  13. package/out/_next/static/chunks/03v5eoc-wic6o.js +1 -0
  14. package/out/_next/static/chunks/03yov._jigv17.js +1 -0
  15. package/out/_next/static/chunks/03~yq9q893hmn.js +1 -0
  16. package/out/_next/static/chunks/08fgie1bcjynm.js +1 -0
  17. package/out/_next/static/chunks/0excsn2a_5qsb.js +4 -0
  18. package/out/_next/static/chunks/0iqqouh_3i5y5.js +13 -0
  19. package/out/_next/static/chunks/0jsosmtclw5n5.js +4 -0
  20. package/out/_next/static/chunks/0ox7p_szjhn69.js +1 -0
  21. package/out/_next/static/chunks/0r7t_sj_sejq9.js +1 -0
  22. package/out/_next/static/chunks/13uu.sohs74zg.js +31 -0
  23. package/out/_next/static/chunks/15kwal..m9r49.css +2 -0
  24. package/out/_next/static/chunks/17oc2l.ekcs8b.css +1 -0
  25. package/out/_next/static/chunks/17sk4qv6_d0co.js +1 -0
  26. package/out/_next/static/chunks/turbopack-06pqx~0d8czn_.js +1 -0
  27. package/out/_next/static/eq3ebKZWXVJquNrlYMOZR/_buildManifest.js +15 -0
  28. package/out/_next/static/eq3ebKZWXVJquNrlYMOZR/_clientMiddlewareManifest.js +1 -0
  29. package/out/_next/static/eq3ebKZWXVJquNrlYMOZR/_ssgManifest.js +1 -0
  30. package/out/_next/static/media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2 +0 -0
  31. package/out/_next/static/media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2 +0 -0
  32. package/out/_next/static/media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2 +0 -0
  33. package/out/_next/static/media/favicon.0x3dzn~oxb6tn.ico +0 -0
  34. package/out/_not-found/__next._full.txt +17 -0
  35. package/out/_not-found/__next._head.txt +6 -0
  36. package/out/_not-found/__next._index.txt +6 -0
  37. package/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
  38. package/out/_not-found/__next._not-found.txt +5 -0
  39. package/out/_not-found/__next._tree.txt +2 -0
  40. package/out/_not-found.html +1 -0
  41. package/out/_not-found.txt +17 -0
  42. package/out/favicon.ico +0 -0
  43. package/out/file.svg +1 -0
  44. package/out/globe.svg +1 -0
  45. package/out/index.html +1 -0
  46. package/out/index.txt +17 -0
  47. package/out/next.svg +1 -0
  48. package/out/project/_/__next._full.txt +20 -0
  49. package/out/project/_/__next._head.txt +6 -0
  50. package/out/project/_/__next._index.txt +6 -0
  51. package/out/project/_/__next._tree.txt +4 -0
  52. package/out/project/_/__next.project.$d$id.__PAGE__.txt +7 -0
  53. package/out/project/_/__next.project.$d$id.txt +5 -0
  54. package/out/project/_/__next.project.txt +5 -0
  55. package/out/project/_/memory/__next._full.txt +19 -0
  56. package/out/project/_/memory/__next._head.txt +6 -0
  57. package/out/project/_/memory/__next._index.txt +6 -0
  58. package/out/project/_/memory/__next._tree.txt +3 -0
  59. package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +6 -0
  60. package/out/project/_/memory/__next.project.$d$id.memory.txt +5 -0
  61. package/out/project/_/memory/__next.project.$d$id.txt +5 -0
  62. package/out/project/_/memory/__next.project.txt +5 -0
  63. package/out/project/_/memory.html +1 -0
  64. package/out/project/_/memory.txt +19 -0
  65. package/out/project/_/queue/__next._full.txt +19 -0
  66. package/out/project/_/queue/__next._head.txt +6 -0
  67. package/out/project/_/queue/__next._index.txt +6 -0
  68. package/out/project/_/queue/__next._tree.txt +3 -0
  69. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +6 -0
  70. package/out/project/_/queue/__next.project.$d$id.queue.txt +5 -0
  71. package/out/project/_/queue/__next.project.$d$id.txt +5 -0
  72. package/out/project/_/queue/__next.project.txt +5 -0
  73. package/out/project/_/queue.html +1 -0
  74. package/out/project/_/queue.txt +19 -0
  75. package/out/project/_.html +1 -0
  76. package/out/project/_.txt +20 -0
  77. package/out/settings/__next._full.txt +19 -0
  78. package/out/settings/__next._head.txt +6 -0
  79. package/out/settings/__next._index.txt +6 -0
  80. package/out/settings/__next._tree.txt +3 -0
  81. package/out/settings/__next.settings.__PAGE__.txt +6 -0
  82. package/out/settings/__next.settings.txt +5 -0
  83. package/out/settings.html +1 -0
  84. package/out/settings.txt +19 -0
  85. package/out/setup/__next._full.txt +19 -0
  86. package/out/setup/__next._head.txt +6 -0
  87. package/out/setup/__next._index.txt +6 -0
  88. package/out/setup/__next._tree.txt +3 -0
  89. package/out/setup/__next.setup.__PAGE__.txt +6 -0
  90. package/out/setup/__next.setup.txt +5 -0
  91. package/out/setup.html +1 -0
  92. package/out/setup.txt +19 -0
  93. package/out/vercel.svg +1 -0
  94. package/out/window.svg +1 -0
  95. package/package.json +61 -0
  96. package/server/config.js +63 -0
  97. package/server/index.js +476 -0
  98. package/server/routes.js +889 -0
  99. package/templates/CLAUDE.md +57 -0
  100. package/templates/config.toml +46 -0
  101. package/templates/seeds/t1.AGENTS.md +55 -0
  102. package/templates/seeds/t2a.AGENTS.md +96 -0
  103. package/templates/seeds/t2b.AGENTS.md +96 -0
  104. package/templates/seeds/t3.AGENTS.md +80 -0
  105. package/templates/wrapper.py +70 -0
@@ -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
+ }