@autoclawd/autoclawd 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Dockerfile CHANGED
@@ -1,18 +1,15 @@
1
- # autoclawd base image — all common runtimes for Claude Code
2
- # Use this as docker.image in your config for zero-setup multi-language support
1
+ # autoclawd Docker image — multi-runtime environment for Claude Code
3
2
  #
4
- # Build: docker build -t autoclawd-base .
5
- # Config: docker: { image: autoclawd-base }
3
+ # Build: docker build -t autoclawd .
4
+ # Config: docker: { image: autoclawd }
6
5
  #
7
- # Includes: Node.js 20, Python 3, Go, Rust, Ruby, Git, common build tools
8
- # Size: ~1.5 GB (trades disk for zero-config developer experience)
6
+ # Includes: Node.js 20, Python 3, Go, Ruby, Git, common build tools
7
+ # Claude Code is auto-installed by autoclawd at runtime — not baked in
9
8
 
10
9
  FROM node:20-bookworm
11
10
 
12
- # Avoid interactive prompts during package installation
13
11
  ENV DEBIAN_FRONTEND=noninteractive
14
12
 
15
- # Common build tools + git (already in node:20 but be explicit)
16
13
  RUN apt-get update && apt-get install -y --no-install-recommends \
17
14
  build-essential \
18
15
  git \
@@ -22,15 +19,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
22
19
  openssh-client \
23
20
  jq \
24
21
  unzip \
25
- # Python
26
22
  python3 \
27
23
  python3-pip \
28
24
  python3-venv \
29
25
  python3-dev \
30
- # Ruby
31
26
  ruby \
32
27
  ruby-dev \
33
- # Go (via official tarball for latest stable)
34
28
  && rm -rf /var/lib/apt/lists/*
35
29
 
36
30
  # Install Go (bookworm packages are often outdated)
@@ -39,19 +33,15 @@ RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-$(dpkg --print-architect
39
33
  | tar -xz -C /usr/local
40
34
  ENV PATH="/usr/local/go/bin:${PATH}"
41
35
 
42
- # Rust is omitted by default (~1.5GB). For Rust repos, use docker.setup:
36
+ # Rust is omitted (~1.5GB). For Rust repos, use docker.setup:
43
37
  # docker:
44
38
  # setup:
45
39
  # - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
46
40
 
47
- # Pre-install Claude Code globally
48
- RUN npm install -g @anthropic-ai/claude-code@latest
49
-
50
41
  # Verify installations
51
42
  RUN node --version && python3 --version && go version && ruby --version && git --version
52
43
 
53
- # Workspace directory
54
44
  WORKDIR /workspace
55
45
 
56
- LABEL org.opencontainers.image.title="autoclawd-base"
57
- LABEL org.opencontainers.image.description="Multi-runtime base image for autoclawd (Linear → Claude Code → PRs)"
46
+ LABEL org.opencontainers.image.title="autoclawd"
47
+ LABEL org.opencontainers.image.description="Multi-runtime environment for autoclawd (Linear → Claude Code → PRs)"
package/dist/index.js CHANGED
@@ -184,6 +184,10 @@ function enableFileLogging() {
184
184
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
185
185
  logFilePath = join2(logDir, `autoclawd-${date}.log`);
186
186
  }
187
+ function getLogFilePath() {
188
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
189
+ return join2(homedir2(), ".autoclawd", "logs", `autoclawd-${date}.log`);
190
+ }
187
191
  function shouldLog(level) {
188
192
  return levels[level] >= levels[currentLevel];
189
193
  }
@@ -535,10 +539,9 @@ import { createHmac, timingSafeEqual } from "crypto";
535
539
  // src/docker.ts
536
540
  import Docker from "dockerode";
537
541
  import { PassThrough } from "stream";
538
- import { homedir as homedir3, tmpdir } from "os";
542
+ import { homedir as homedir3 } from "os";
539
543
  import { join as join3 } from "path";
540
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdtempSync, rmSync } from "fs";
541
- import { execSync } from "child_process";
544
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
542
545
  var docker = new Docker();
543
546
  async function checkDockerAvailable() {
544
547
  try {
@@ -549,20 +552,6 @@ async function checkDockerAvailable() {
549
552
  );
550
553
  }
551
554
  }
552
- var AUTOCODE_BASE_DOCKERFILE = `FROM node:20-bookworm
553
- ENV DEBIAN_FRONTEND=noninteractive
554
- RUN apt-get update && apt-get install -y --no-install-recommends \\
555
- build-essential git curl wget ca-certificates openssh-client jq unzip \\
556
- python3 python3-pip python3-venv python3-dev \\
557
- ruby ruby-dev \\
558
- && rm -rf /var/lib/apt/lists/*
559
- ARG GO_VERSION=1.22.2
560
- RUN curl -fsSL "https://go.dev/dl/go\${GO_VERSION}.linux-$(dpkg --print-architecture).tar.gz" \\
561
- | tar -xz -C /usr/local
562
- ENV PATH="/usr/local/go/bin:\${PATH}"
563
- RUN npm install -g @anthropic-ai/claude-code@latest
564
- WORKDIR /workspace
565
- `;
566
555
  async function ensureImage(image) {
567
556
  try {
568
557
  await docker.getImage(image).inspect();
@@ -570,28 +559,6 @@ async function ensureImage(image) {
570
559
  return;
571
560
  } catch {
572
561
  }
573
- if (image === "autoclawd-base") {
574
- log.info("Building autoclawd-base image (first time only, takes a few minutes)...");
575
- const buildDir = mkdtempSync(join3(tmpdir(), "autoclawd-build-"));
576
- try {
577
- writeFileSync(join3(buildDir, "Dockerfile"), AUTOCODE_BASE_DOCKERFILE);
578
- execSync(`docker build -t autoclawd-base ${buildDir}`, {
579
- stdio: "inherit",
580
- timeout: 6e5
581
- });
582
- log.success("autoclawd-base image built");
583
- return;
584
- } catch (err) {
585
- throw new Error(
586
- `Failed to build autoclawd-base image.
587
- You can build manually: docker build -t autoclawd-base .
588
- Or use a different image in your config (e.g. node:20).
589
- Error: ${err instanceof Error ? err.message : err}`
590
- );
591
- } finally {
592
- rmSync(buildDir, { recursive: true, force: true });
593
- }
594
- }
595
562
  log.info(`Pulling image ${image} (this may take a moment)...`);
596
563
  await retry(() => new Promise((resolve, reject) => {
597
564
  docker.pull(image, (err, stream) => {
@@ -1058,10 +1025,10 @@ async function commitAndPush(container, opts) {
1058
1025
  }
1059
1026
 
1060
1027
  // src/worker.ts
1061
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1028
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
1062
1029
  import { join as join5 } from "path";
1063
- import { tmpdir as tmpdir2 } from "os";
1064
- import { execSync as execSync2 } from "child_process";
1030
+ import { tmpdir } from "os";
1031
+ import { execSync } from "child_process";
1065
1032
 
1066
1033
  // src/db.ts
1067
1034
  import Database from "better-sqlite3";
@@ -1231,9 +1198,9 @@ function assertSafeRef(name, label) {
1231
1198
  }
1232
1199
  }
1233
1200
  function createAskpass(githubToken) {
1234
- const dir = mkdtempSync2(join5(tmpdir2(), "autoclawd-cred-"));
1201
+ const dir = mkdtempSync(join5(tmpdir(), "autoclawd-cred-"));
1235
1202
  const path = join5(dir, "askpass.sh");
1236
- writeFileSync2(path, `#!/bin/sh
1203
+ writeFileSync(path, `#!/bin/sh
1237
1204
  echo "${githubToken}"
1238
1205
  `, { mode: 448 });
1239
1206
  return { dir, path };
@@ -1245,7 +1212,7 @@ function detectDefaultBranch(repoUrl, githubToken) {
1245
1212
  const askpass = createAskpass(githubToken);
1246
1213
  const authedUrl = repoUrl.replace("https://", "https://x-access-token@");
1247
1214
  try {
1248
- const output = execSync2(
1215
+ const output = execSync(
1249
1216
  `git ls-remote --symref -- ${authedUrl} HEAD`,
1250
1217
  { stdio: "pipe", timeout: 3e4, env: gitEnv(askpass.path), encoding: "utf-8" }
1251
1218
  );
@@ -1253,27 +1220,27 @@ function detectDefaultBranch(repoUrl, githubToken) {
1253
1220
  if (match) return match[1];
1254
1221
  } catch {
1255
1222
  } finally {
1256
- rmSync2(askpass.dir, { recursive: true, force: true });
1223
+ rmSync(askpass.dir, { recursive: true, force: true });
1257
1224
  }
1258
1225
  return "main";
1259
1226
  }
1260
1227
  async function cloneToTemp(repoUrl, baseBranch, githubToken) {
1261
1228
  assertSafeRef(baseBranch, "base branch");
1262
1229
  return retrySync(() => {
1263
- const workDir = mkdtempSync2(join5(tmpdir2(), "autoclawd-"));
1230
+ const workDir = mkdtempSync(join5(tmpdir(), "autoclawd-"));
1264
1231
  const askpass = createAskpass(githubToken);
1265
1232
  const authedUrl = repoUrl.replace("https://", "https://x-access-token@");
1266
1233
  try {
1267
- execSync2(`git clone --depth=50 -b ${baseBranch} -- ${authedUrl} ${workDir}`, {
1234
+ execSync(`git clone --depth=50 -b ${baseBranch} -- ${authedUrl} ${workDir}`, {
1268
1235
  stdio: "pipe",
1269
1236
  timeout: 12e4,
1270
1237
  env: gitEnv(askpass.path)
1271
1238
  });
1272
1239
  } catch (err) {
1273
- rmSync2(workDir, { recursive: true, force: true });
1240
+ rmSync(workDir, { recursive: true, force: true });
1274
1241
  throw err;
1275
1242
  } finally {
1276
- rmSync2(askpass.dir, { recursive: true, force: true });
1243
+ rmSync(askpass.dir, { recursive: true, force: true });
1277
1244
  }
1278
1245
  return workDir;
1279
1246
  }, { label: "git clone", retryIf: isTransientError });
@@ -1409,10 +1376,10 @@ async function executeTicket(opts) {
1409
1376
  log.ticket(ticket.identifier, `Stacked on branch: ${ticket.baseBranch}`);
1410
1377
  }
1411
1378
  if (actualBase !== detectedBase) {
1412
- rmSync2(workDir, { recursive: true, force: true });
1379
+ rmSync(workDir, { recursive: true, force: true });
1413
1380
  workDir = await cloneToTemp(ticket.repoUrl, actualBase, config.github.token);
1414
1381
  }
1415
- execSync2(`git checkout -B ${branchName} --`, { cwd: workDir, stdio: "pipe" });
1382
+ execSync(`git checkout -B ${branchName} --`, { cwd: workDir, stdio: "pipe" });
1416
1383
  container = await createContainer({
1417
1384
  dockerConfig: docker2,
1418
1385
  workspacePath: workDir,
@@ -1579,7 +1546,7 @@ async function executeTicket(opts) {
1579
1546
  }
1580
1547
  if (workDir) {
1581
1548
  try {
1582
- rmSync2(workDir, { recursive: true, force: true });
1549
+ rmSync(workDir, { recursive: true, force: true });
1583
1550
  } catch {
1584
1551
  }
1585
1552
  }
@@ -1668,7 +1635,7 @@ async function fixPR(opts) {
1668
1635
  if (container) await destroyContainer(container);
1669
1636
  if (workDir) {
1670
1637
  try {
1671
- rmSync2(workDir, { recursive: true, force: true });
1638
+ rmSync(workDir, { recursive: true, force: true });
1672
1639
  } catch {
1673
1640
  }
1674
1641
  }
@@ -2143,7 +2110,7 @@ function printHistoryTable(records) {
2143
2110
  }
2144
2111
 
2145
2112
  // src/deps.ts
2146
- import { execSync as execSync3 } from "child_process";
2113
+ import { execSync as execSync2 } from "child_process";
2147
2114
  import { existsSync as existsSync6 } from "fs";
2148
2115
  import { join as join7 } from "path";
2149
2116
  import { homedir as homedir5 } from "os";
@@ -2158,7 +2125,7 @@ function detectPackageManager() {
2158
2125
  ];
2159
2126
  for (const [name, cmd] of checks) {
2160
2127
  try {
2161
- execSync3(`which ${cmd}`, { stdio: "pipe" });
2128
+ execSync2(`which ${cmd}`, { stdio: "pipe" });
2162
2129
  return name;
2163
2130
  } catch {
2164
2131
  }
@@ -2249,7 +2216,7 @@ function installDep(dep) {
2249
2216
  if (dep.installCommands.length === 0) return false;
2250
2217
  for (const cmd of dep.installCommands) {
2251
2218
  try {
2252
- execSync3(cmd, { stdio: "inherit", timeout: 3e5 });
2219
+ execSync2(cmd, { stdio: "inherit", timeout: 3e5 });
2253
2220
  } catch {
2254
2221
  return false;
2255
2222
  }
@@ -2259,15 +2226,15 @@ function installDep(dep) {
2259
2226
  function addUserToDockerGroup() {
2260
2227
  if (process.platform !== "linux") return;
2261
2228
  try {
2262
- const user = execSync3("whoami", { encoding: "utf-8" }).trim();
2263
- execSync3(`sudo usermod -aG docker ${user}`, { stdio: "pipe" });
2229
+ const user = execSync2("whoami", { encoding: "utf-8" }).trim();
2230
+ execSync2(`sudo usermod -aG docker ${user}`, { stdio: "pipe" });
2264
2231
  log.info(`Added ${user} to docker group \u2014 log out and back in to apply`);
2265
2232
  } catch {
2266
2233
  }
2267
2234
  }
2268
2235
  function commandExists(cmd) {
2269
2236
  try {
2270
- execSync3(`which ${cmd}`, { stdio: "pipe" });
2237
+ execSync2(`which ${cmd}`, { stdio: "pipe" });
2271
2238
  return true;
2272
2239
  } catch {
2273
2240
  return false;
@@ -2275,7 +2242,7 @@ function commandExists(cmd) {
2275
2242
  }
2276
2243
  function isDockerRunning() {
2277
2244
  try {
2278
- execSync3("docker info", { stdio: "pipe", timeout: 1e4 });
2245
+ execSync2("docker info", { stdio: "pipe", timeout: 1e4 });
2279
2246
  return true;
2280
2247
  } catch {
2281
2248
  return false;
@@ -2283,8 +2250,8 @@ function isDockerRunning() {
2283
2250
  }
2284
2251
 
2285
2252
  // src/index.ts
2286
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
2287
- import { execSync as execSync4 } from "child_process";
2253
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
2254
+ import { execSync as execSync3, spawn as spawn2 } from "child_process";
2288
2255
  import { createInterface } from "readline";
2289
2256
  import { join as join8 } from "path";
2290
2257
  import { homedir as homedir6 } from "os";
@@ -2294,6 +2261,36 @@ import { createRequire } from "module";
2294
2261
  var require2 = createRequire(import.meta.url);
2295
2262
  var pkg = require2("../package.json");
2296
2263
  var program = new Command();
2264
+ var WATCHER_PID_FILE = join8(homedir6(), ".autoclawd", "watcher.pid");
2265
+ function readWatcherPid() {
2266
+ try {
2267
+ const content = readFileSync4(WATCHER_PID_FILE, "utf-8").trim();
2268
+ const pid = parseInt(content, 10);
2269
+ return isNaN(pid) ? null : pid;
2270
+ } catch {
2271
+ return null;
2272
+ }
2273
+ }
2274
+ function writeWatcherPid(pid) {
2275
+ mkdirSync3(join8(homedir6(), ".autoclawd"), { recursive: true });
2276
+ writeFileSync2(WATCHER_PID_FILE, String(pid), { mode: 384 });
2277
+ }
2278
+ function removeWatcherPid() {
2279
+ try {
2280
+ unlinkSync(WATCHER_PID_FILE);
2281
+ } catch {
2282
+ }
2283
+ }
2284
+ function getRunningWatcherPid() {
2285
+ const pid = readWatcherPid();
2286
+ if (pid === null) return null;
2287
+ try {
2288
+ process.kill(pid, 0);
2289
+ return pid;
2290
+ } catch {
2291
+ return null;
2292
+ }
2293
+ }
2297
2294
  program.name("autoclawd").description("Linear webhooks \u2192 Claude Code in Docker \u2192 PRs").version(pkg.version);
2298
2295
  function ask(question, defaultVal) {
2299
2296
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -2367,7 +2364,29 @@ program.command("serve").description("Start webhook server with auto-tunnel").op
2367
2364
  process.on("SIGINT", shutdown);
2368
2365
  process.on("SIGTERM", shutdown);
2369
2366
  });
2370
- program.command("watch").description("Poll Linear for tickets (no webhook/tunnel needed)").option("-c, --config <path>", "Config file path").option("-i, --interval <seconds>", "Poll interval in seconds", "30").option("--once", "Poll once and exit (useful for cron)").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2367
+ program.command("watch").description("Poll Linear for tickets (no webhook/tunnel needed)").option("-c, --config <path>", "Config file path").option("-i, --interval <seconds>", "Poll interval in seconds", "30").option("--once", "Poll once and exit (useful for cron)").option("--foreground", "Run in foreground instead of daemonizing").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2368
+ if (!opts.foreground && !opts.once) {
2369
+ const existingPid = getRunningWatcherPid();
2370
+ if (existingPid !== null) {
2371
+ console.error(`autoclawd watcher is already running (PID ${existingPid})`);
2372
+ console.error(`Run 'autoclawd stop' to stop it, or use '--foreground' to run in terminal`);
2373
+ process.exit(1);
2374
+ }
2375
+ const child = spawn2(process.argv[0], [...process.argv.slice(1), "--foreground"], {
2376
+ detached: true,
2377
+ stdio: "ignore"
2378
+ });
2379
+ await new Promise((resolve, reject) => {
2380
+ child.once("spawn", resolve);
2381
+ child.once("error", reject);
2382
+ });
2383
+ writeWatcherPid(child.pid);
2384
+ const logPath = getLogFilePath();
2385
+ console.log(`autoclawd watching in background (PID ${child.pid}, log: ${logPath})`);
2386
+ child.unref();
2387
+ process.exit(0);
2388
+ return;
2389
+ }
2371
2390
  if (opts.verbose) setLogLevel("debug");
2372
2391
  enableFileLogging();
2373
2392
  log.info(`autoclawd v${pkg.version}`);
@@ -2390,12 +2409,34 @@ program.command("watch").description("Poll Linear for tickets (no webhook/tunnel
2390
2409
  const shutdown = () => {
2391
2410
  log.info("Shutting down...");
2392
2411
  watcher.stop();
2412
+ removeWatcherPid();
2393
2413
  process.exit(0);
2394
2414
  };
2395
2415
  process.on("SIGINT", shutdown);
2396
2416
  process.on("SIGTERM", shutdown);
2397
2417
  await watcher.start(intervalSeconds, opts.once ?? false);
2398
2418
  });
2419
+ program.command("stop").description("Stop the background watcher").action(() => {
2420
+ const pid = readWatcherPid();
2421
+ if (pid === null) {
2422
+ log.info("No watcher PID file found \u2014 watcher is not running");
2423
+ return;
2424
+ }
2425
+ const running = getRunningWatcherPid();
2426
+ if (running === null) {
2427
+ removeWatcherPid();
2428
+ log.info(`Removed stale PID file (process ${pid} is not running)`);
2429
+ return;
2430
+ }
2431
+ try {
2432
+ process.kill(pid, "SIGTERM");
2433
+ removeWatcherPid();
2434
+ log.success(`Watcher stopped (PID ${pid})`);
2435
+ } catch (err) {
2436
+ log.error(`Failed to stop watcher: ${err instanceof Error ? err.message : err}`);
2437
+ process.exit(1);
2438
+ }
2439
+ });
2399
2440
  program.command("run <ticket>").description("Run a single ticket (e.g. autoclawd run RAH-123)").option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose logging").option("--dry-run", "Show what would happen without executing").option("--force", "Bypass completed-ticket check and re-run").action(async (ticketId, opts) => {
2400
2441
  if (opts.verbose) setLogLevel("debug");
2401
2442
  enableFileLogging();
@@ -2703,7 +2744,7 @@ program.command("init").description("Set up autoclawd interactively").action(asy
2703
2744
  }
2704
2745
  let ghToken = "";
2705
2746
  try {
2706
- ghToken = execSync4("gh auth token", { encoding: "utf-8" }).trim();
2747
+ ghToken = execSync3("gh auth token", { encoding: "utf-8" }).trim();
2707
2748
  log.success(`GitHub token detected from gh CLI`);
2708
2749
  } catch {
2709
2750
  ghToken = await ask("GitHub personal access token");
@@ -2719,19 +2760,11 @@ program.command("init").description("Set up autoclawd interactively").action(asy
2719
2760
  log.error("GitHub token is invalid or expired");
2720
2761
  process.exit(1);
2721
2762
  }
2722
- console.log("\n Docker image options:");
2723
- console.log(" autoclawd-base \u2014 All runtimes: Node, Python, Go, Rust, Ruby (~1.5GB)");
2724
- console.log(" Works with any repo, no per-repo config needed");
2725
- console.log(" node:20 \u2014 Node.js only (fast, ~300MB)");
2726
- const dockerImage = await ask("Docker image", "autoclawd-base");
2727
- if (dockerImage === "autoclawd-base") {
2728
- try {
2729
- execSync4("docker image inspect autoclawd-base", { stdio: "pipe" });
2730
- log.success("autoclawd-base image found");
2731
- } catch {
2732
- log.info("autoclawd-base will be built automatically on first run (~3 min)");
2733
- }
2734
- }
2763
+ console.log("\n Docker image (any image works \u2014 autoclawd auto-installs git + Claude Code):");
2764
+ console.log(" node:20 \u2014 Node.js (default, recommended)");
2765
+ console.log(" python:3.12 \u2014 Python");
2766
+ console.log(" ubuntu:24.04 \u2014 General purpose");
2767
+ const dockerImage = await ask("Docker image", "node:20");
2735
2768
  const model = await ask("Claude model", "claude-sonnet-4-6");
2736
2769
  const maxIter = await ask("Max iterations per ticket", "10");
2737
2770
  const config = `# autoclawd config \u2014 generated by autoclawd init
@@ -2762,7 +2795,7 @@ agent:
2762
2795
 
2763
2796
  maxConcurrent: 1
2764
2797
  `;
2765
- writeFileSync3(CONFIG_FILE, config, { mode: 384 });
2798
+ writeFileSync2(CONFIG_FILE, config, { mode: 384 });
2766
2799
  log.success(`Config saved to ${CONFIG_FILE}`);
2767
2800
  console.log(`
2768
2801
  Setup complete! Next steps: