liutaio 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/docker/run.sh ADDED
@@ -0,0 +1,366 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # ─── Liutaio — run Claude Code loops with auto-detecting auth ───
5
+ #
6
+ # Usage:
7
+ # liutaio <agents-file> <iterations> <base-branch> [options]
8
+ #
9
+ # Authentication (checked in this order):
10
+ # 1. Cached OAuth credentials (Docker volume from a previous --oauth run)
11
+ # 2. Host credentials (macOS Keychain or ~/.claude/.credentials.json)
12
+ # 3. ANTHROPIC_API_KEY env var
13
+ # 4. Interactive OAuth login (prompts user to authorise in browser)
14
+ #
15
+ # Options:
16
+ # --interactive Run in foreground (default: detached with log tailing)
17
+ # --oauth Force interactive OAuth login (skip host credentials)
18
+ # --fresh-login Clear cached OAuth and re-authenticate
19
+ # --rebuild Force rebuild the Docker image
20
+ # --dry-run Print the docker run command without executing
21
+ # --name NAME Container name (default: liutaio-<base-branch>)
22
+ # --node-version V Node.js version for the Docker image (default: 22)
23
+ # --repo PATH Path to the git repository (default: auto-detect)
24
+ # --env KEY=VALUE Pass env var into the container (repeatable)
25
+ # ─────────────────────────────────────────────────────────────────────
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
28
+
29
+ IMAGE_NAME="liutaio"
30
+ CREDS_VOLUME="liutaio-creds"
31
+
32
+ # ─── Parse arguments ─────────────────────────────────────────────────
33
+ AGENTS_FILE=""
34
+ ITERATIONS=""
35
+ BASE_BRANCH=""
36
+ INTERACTIVE=false
37
+ REBUILD=false
38
+ DRY_RUN=false
39
+ CONTAINER_NAME=""
40
+ NODE_VERSION="${LIUTAIO_NODE_VERSION:-22}"
41
+ REPO_ROOT=""
42
+ EXTRA_ENVS=()
43
+ FORCE_OAUTH=false
44
+ FRESH_LOGIN=false
45
+
46
+ while [[ $# -gt 0 ]]; do
47
+ case $1 in
48
+ --interactive) INTERACTIVE=true; shift ;;
49
+ --oauth) FORCE_OAUTH=true; shift ;;
50
+ --fresh-login) FRESH_LOGIN=true; FORCE_OAUTH=true; shift ;;
51
+ --rebuild) REBUILD=true; shift ;;
52
+ --dry-run) DRY_RUN=true; shift ;;
53
+ --name) CONTAINER_NAME="$2"; shift 2 ;;
54
+ --node-version) NODE_VERSION="$2"; shift 2 ;;
55
+ --repo) REPO_ROOT="$2"; shift 2 ;;
56
+ --env) EXTRA_ENVS+=("$2"); shift 2 ;;
57
+ -*) echo "Unknown option: $1"; exit 1 ;;
58
+ *)
59
+ if [ -z "$AGENTS_FILE" ]; then AGENTS_FILE="$1"
60
+ elif [ -z "$ITERATIONS" ]; then ITERATIONS="$1"
61
+ elif [ -z "$BASE_BRANCH" ]; then BASE_BRANCH="$1"
62
+ fi
63
+ shift ;;
64
+ esac
65
+ done
66
+
67
+ if [ -z "$AGENTS_FILE" ] || [ -z "$ITERATIONS" ] || [ -z "$BASE_BRANCH" ]; then
68
+ echo "Liutaio — run Claude Code loops with auto-detecting auth"
69
+ echo ""
70
+ echo "Usage:"
71
+ echo " liutaio <agents-file> <iterations> <base-branch> [options]"
72
+ echo ""
73
+ echo "Arguments:"
74
+ echo " agents-file Path to agents.md relative to repo root"
75
+ echo " iterations Number of loop iterations"
76
+ echo " base-branch Name of the base branch to create from main"
77
+ echo ""
78
+ echo "Options:"
79
+ echo " --interactive Run in foreground with attached TTY"
80
+ echo " --oauth Force interactive OAuth login (skip host credentials)"
81
+ echo " --fresh-login Clear cached OAuth and re-authenticate"
82
+ echo " --rebuild Force rebuild the Docker image"
83
+ echo " --dry-run Print docker command without executing"
84
+ echo " --name NAME Container name (default: liutaio-<base-branch>)"
85
+ echo " --node-version V Node.js version (default: 22, or LIUTAIO_NODE_VERSION)"
86
+ echo " --repo PATH Path to git repo (default: auto-detect from cwd)"
87
+ echo " --env KEY=VALUE Pass env var into the container (repeatable)"
88
+ echo ""
89
+ echo "Authentication (checked in this order):"
90
+ echo " 1. Cached OAuth credentials (Docker volume from a previous --oauth run)"
91
+ echo " 2. Host credentials (macOS Keychain or ~/.claude/.credentials.json)"
92
+ echo " 3. ANTHROPIC_API_KEY env var"
93
+ echo " 4. Interactive OAuth login (prompts in the terminal)"
94
+ echo ""
95
+ echo "Examples:"
96
+ echo " liutaio agents.md 10 my-branch # auto-detect auth"
97
+ echo " liutaio agents.md 10 my-branch --oauth # force OAuth login"
98
+ echo " liutaio agents.md 10 my-branch --fresh-login # re-authenticate"
99
+ exit 1
100
+ fi
101
+
102
+ # ─── Detect repo root ───────────────────────────────────────────────
103
+ if [ -z "$REPO_ROOT" ]; then
104
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
105
+ if [ -z "$REPO_ROOT" ]; then
106
+ echo "Error: not inside a git repository. Use --repo to specify the path."
107
+ exit 1
108
+ fi
109
+ fi
110
+
111
+ CONTAINER_NAME="${CONTAINER_NAME:-liutaio-${BASE_BRANCH}}"
112
+
113
+ # ─── Validate agents file exists ─────────────────────────────────────
114
+ if [ ! -f "$REPO_ROOT/$AGENTS_FILE" ]; then
115
+ echo "Error: agents file not found: $REPO_ROOT/$AGENTS_FILE"
116
+ exit 1
117
+ fi
118
+
119
+ # ─── Build image ─────────────────────────────────────────────────────
120
+ if $REBUILD || ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
121
+ echo "Building Liutaio image (Node $NODE_VERSION)..."
122
+ docker build --build-arg NODE_VERSION="$NODE_VERSION" -t "$IMAGE_NAME" "$SCRIPT_DIR"
123
+ echo ""
124
+ fi
125
+
126
+ # ─── Ensure credentials volume exists ───────────────────────────────
127
+ docker volume create "$CREDS_VOLUME" &>/dev/null || true
128
+
129
+ if $FRESH_LOGIN; then
130
+ echo "Clearing cached credentials (--fresh-login)..."
131
+ docker run --rm -v "$CREDS_VOLUME:/credentials" alpine sh -c "rm -f /credentials/credentials.json"
132
+ fi
133
+
134
+ # ─── Resolve authentication method ──────────────────────────────────
135
+ # Determines: AUTH_METHOD, CREDS_FILE, USE_API_KEY, NEEDS_TTY
136
+
137
+ AUTH_METHOD=""
138
+ CREDS_FILE=""
139
+ USE_API_KEY=false
140
+ NEEDS_TTY=false
141
+
142
+ cleanup_creds() {
143
+ if [ -n "$CREDS_FILE" ] && [ -f "$CREDS_FILE" ]; then
144
+ rm -f "$CREDS_FILE"
145
+ fi
146
+ }
147
+ trap cleanup_creds EXIT
148
+
149
+ resolve_auth() {
150
+ # --oauth skips host credentials, goes straight to cached OAuth or interactive
151
+ if ! $FORCE_OAUTH; then
152
+
153
+ # 1. Host credentials: macOS Keychain
154
+ if [ "$(uname)" = "Darwin" ]; then
155
+ local keychain_data
156
+ keychain_data=$(security find-generic-password -s "Claude Code-credentials" -a "$(whoami)" -w 2>/dev/null || true)
157
+ if [ -n "$keychain_data" ]; then
158
+ CREDS_FILE=$(mktemp "${TMPDIR:-/tmp}/liutaio-creds-XXXXXX")
159
+ chmod 600 "$CREDS_FILE"
160
+ echo "$keychain_data" > "$CREDS_FILE"
161
+ if jq -e '.claudeAiOauth.accessToken and .claudeAiOauth.refreshToken' "$CREDS_FILE" >/dev/null 2>&1; then
162
+ AUTH_METHOD="keychain"
163
+ return 0
164
+ fi
165
+ rm -f "$CREDS_FILE"
166
+ CREDS_FILE=""
167
+ fi
168
+ fi
169
+
170
+ # 2. Host credentials: ~/.claude/.credentials.json
171
+ if [ -f "$HOME/.claude/.credentials.json" ]; then
172
+ CREDS_FILE=$(mktemp "${TMPDIR:-/tmp}/liutaio-creds-XXXXXX")
173
+ chmod 600 "$CREDS_FILE"
174
+ cp "$HOME/.claude/.credentials.json" "$CREDS_FILE"
175
+ if jq -e '.claudeAiOauth.accessToken' "$CREDS_FILE" >/dev/null 2>&1; then
176
+ AUTH_METHOD="credentials-file"
177
+ return 0
178
+ fi
179
+ rm -f "$CREDS_FILE"
180
+ CREDS_FILE=""
181
+ fi
182
+
183
+ # 3. API key
184
+ if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
185
+ AUTH_METHOD="api-key"
186
+ USE_API_KEY=true
187
+ return 0
188
+ fi
189
+
190
+ fi
191
+
192
+ # 4. Cached OAuth (Docker volume)
193
+ local has_cached
194
+ has_cached=$(docker run --rm -v "$CREDS_VOLUME:/credentials" alpine sh -c \
195
+ "test -f /credentials/credentials.json && echo yes || echo no")
196
+ if [ "$has_cached" = "yes" ]; then
197
+ AUTH_METHOD="cached-oauth"
198
+ return 0
199
+ fi
200
+
201
+ # 5. Interactive OAuth (fallback) — needs TTY
202
+ AUTH_METHOD="interactive-oauth"
203
+ NEEDS_TTY=true
204
+ return 0
205
+ }
206
+
207
+ resolve_auth
208
+
209
+ # ─── Read git user from host ─────────────────────────────────────────
210
+ GIT_USER_NAME=$(git config user.name 2>/dev/null || echo "Liutaio Agent")
211
+ GIT_USER_EMAIL=$(git config user.email 2>/dev/null || echo "liutaio@users.noreply.github.com")
212
+
213
+ # ─── Resolve SSH key path ────────────────────────────────────────────
214
+ SSH_DIR="$HOME/.ssh"
215
+ if [ ! -d "$SSH_DIR" ]; then
216
+ echo "Warning: ~/.ssh not found — git push from container will not work"
217
+ fi
218
+
219
+ # ─── Remove existing container with same name ────────────────────────
220
+ if docker container inspect "$CONTAINER_NAME" &>/dev/null; then
221
+ echo "Removing existing container '$CONTAINER_NAME'..."
222
+ docker rm -f "$CONTAINER_NAME" &>/dev/null
223
+ fi
224
+
225
+ # ─── Resolve output directory ────────────────────────────────────────
226
+ AGENTS_DIR=$(dirname "$AGENTS_FILE")
227
+ OUTPUT_DIR="$REPO_ROOT/$AGENTS_DIR"
228
+ mkdir -p "$OUTPUT_DIR"
229
+
230
+ # ─── Assemble docker run command ─────────────────────────────────────
231
+ DOCKER_CMD=(
232
+ docker run
233
+ --name "$CONTAINER_NAME"
234
+ --hostname liutaio
235
+ --init
236
+
237
+ # Repo mounted read-only as clone source
238
+ -v "$REPO_ROOT:/repo:ro"
239
+
240
+ # SSH keys
241
+ -v "$SSH_DIR:/ssh-keys:ro"
242
+
243
+ # Output directory
244
+ -v "$OUTPUT_DIR:/output"
245
+
246
+ # Git identity
247
+ -e "GIT_USER_NAME=$GIT_USER_NAME"
248
+ -e "GIT_USER_EMAIL=$GIT_USER_EMAIL"
249
+
250
+ # Memory limit
251
+ --memory=16g
252
+ )
253
+
254
+ # Tell the entrypoint which auth method is in use (controls token refresh behaviour)
255
+ DOCKER_CMD+=(-e "LIUTAIO_AUTH_METHOD=$AUTH_METHOD")
256
+
257
+ # Credentials: mount host file (read-only) OR Docker volume (read-write for OAuth caching)
258
+ if [ -n "$CREDS_FILE" ]; then
259
+ # Host credentials — mount as read-only file, no volume needed
260
+ DOCKER_CMD+=(-v "$CREDS_FILE:/credentials/credentials.json:ro")
261
+ else
262
+ # OAuth or no-creds — mount the volume so the entrypoint can cache credentials
263
+ DOCKER_CMD+=(-v "$CREDS_VOLUME:/credentials")
264
+ fi
265
+
266
+ # Pass API key if using that method
267
+ if $USE_API_KEY; then
268
+ DOCKER_CMD+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
269
+ fi
270
+
271
+ # SSH hosts override
272
+ if [ -n "${LIUTAIO_SSH_HOSTS:-}" ]; then
273
+ DOCKER_CMD+=(-e "LIUTAIO_SSH_HOSTS=$LIUTAIO_SSH_HOSTS")
274
+ fi
275
+
276
+ # Pass ANTHROPIC_API_KEY as fallback even for OAuth modes
277
+ if ! $USE_API_KEY && [ -n "${ANTHROPIC_API_KEY:-}" ]; then
278
+ DOCKER_CMD+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
279
+ fi
280
+
281
+ # User-provided env vars
282
+ if [ ${#EXTRA_ENVS[@]} -gt 0 ]; then
283
+ for env_pair in "${EXTRA_ENVS[@]}"; do
284
+ DOCKER_CMD+=(-e "$env_pair")
285
+ done
286
+ fi
287
+
288
+ # Image and arguments
289
+ DOCKER_CMD+=(
290
+ "$IMAGE_NAME"
291
+ "$AGENTS_FILE" "$ITERATIONS" "$BASE_BRANCH"
292
+ )
293
+
294
+ # Determine run mode: interactive OAuth needs -it, otherwise respect --interactive
295
+ if $NEEDS_TTY || $INTERACTIVE; then
296
+ DOCKER_CMD=("${DOCKER_CMD[@]:0:2}" -it "${DOCKER_CMD[@]:2}")
297
+ else
298
+ DOCKER_CMD=("${DOCKER_CMD[@]:0:2}" -d "${DOCKER_CMD[@]:2}")
299
+ fi
300
+
301
+ # ─── Dry run ─────────────────────────────────────────────────────────
302
+ if $DRY_RUN; then
303
+ echo "Dry run — would execute:"
304
+ echo ""
305
+ PREV_WAS_ENV=false
306
+ for arg in "${DOCKER_CMD[@]}"; do
307
+ if $PREV_WAS_ENV; then
308
+ KEY="${arg%%=*}"
309
+ if echo "$KEY" | grep -qiE '(TOKEN|CREDENTIAL|SECRET|KEY|PASSWORD)'; then
310
+ printf ' %s=***MASKED*** \\\n' "$KEY"
311
+ else
312
+ printf ' %s \\\n' "$arg"
313
+ fi
314
+ PREV_WAS_ENV=false
315
+ elif [ "$arg" = "-e" ]; then
316
+ printf ' %s' "$arg "
317
+ PREV_WAS_ENV=true
318
+ elif [[ "$arg" == *credentials* ]]; then
319
+ printf ' %s \\\n' "***CREDENTIALS_FILE***"
320
+ else
321
+ printf ' %s \\\n' "$arg"
322
+ fi
323
+ done
324
+ exit 0
325
+ fi
326
+
327
+ # ─── Display auth method ────────────────────────────────────────────
328
+ AUTH_DISPLAY=""
329
+ case "$AUTH_METHOD" in
330
+ keychain) AUTH_DISPLAY="macOS Keychain" ;;
331
+ credentials-file) AUTH_DISPLAY="~/.claude/.credentials.json" ;;
332
+ api-key) AUTH_DISPLAY="ANTHROPIC_API_KEY" ;;
333
+ cached-oauth) AUTH_DISPLAY="cached OAuth (use --fresh-login to re-auth)" ;;
334
+ interactive-oauth) AUTH_DISPLAY="interactive OAuth (will prompt)" ;;
335
+ esac
336
+
337
+ # ─── Run ─────────────────────────────────────────────────────────────
338
+ echo "============================================"
339
+ echo " Liutaio"
340
+ echo "============================================"
341
+ echo " Container : $CONTAINER_NAME"
342
+ echo " Agents : $AGENTS_FILE"
343
+ echo " Iterations : $ITERATIONS"
344
+ echo " Base branch: $BASE_BRANCH"
345
+ echo " Node : $NODE_VERSION"
346
+ echo " Output : $OUTPUT_DIR"
347
+ echo " Auth : $AUTH_DISPLAY"
348
+ echo " Mode : $(if $NEEDS_TTY || $INTERACTIVE; then echo 'interactive'; else echo 'detached'; fi)"
349
+ echo "============================================"
350
+ echo ""
351
+
352
+ "${DOCKER_CMD[@]}"
353
+
354
+ # If detached, tail logs
355
+ if ! $NEEDS_TTY && ! $INTERACTIVE; then
356
+ echo "Container started. Useful commands:"
357
+ echo ""
358
+ echo " docker logs -f $CONTAINER_NAME # tail logs"
359
+ echo " docker exec -it $CONTAINER_NAME bash # shell into container"
360
+ echo " docker stop $CONTAINER_NAME # stop gracefully"
361
+ echo " docker rm -f $CONTAINER_NAME # force remove"
362
+ echo ""
363
+ echo "Tailing logs now (Ctrl+C to detach, container keeps running)..."
364
+ echo ""
365
+ docker logs -f "$CONTAINER_NAME"
366
+ fi
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "liutaio",
3
+ "version": "0.1.0",
4
+ "description": "Run AI coding agents in Docker containers — autonomously, safely, and with zero setup.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "liutaio": "./bin/liutaio"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "docker/",
12
+ "agents-template.md",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "ai",
18
+ "agent",
19
+ "claude",
20
+ "docker",
21
+ "sandbox",
22
+ "automation",
23
+ "coding-agent"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/narleybrittes/liutaio"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ }
32
+ }