Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d57630e
fix: resolve channel names via Slack API for all sessions
AztecBot Mar 9, 2026
4dc1515
feat: show artifacts and last reply on dashboard cards
AztecBot Mar 9, 2026
d5d54a6
feat: consistent collapsed cards with artifacts and reply preview
AztecBot Mar 9, 2026
69a66c0
fix: clone_repo defaults to correct branch per profile, softer fallba…
AztecBot Mar 9, 2026
ca11562
docs: strengthen session_status guidance in all profile prompts
AztecBot Mar 9, 2026
a6b1040
fix: scroll to bottom on workspace page load
AztecBot Mar 9, 2026
d7d5277
docs: session_status must be the very first action in all profiles
AztecBot Mar 9, 2026
a280601
feat: session recovery, activity dedup, creds CLI, workspace UI impro…
AztecBot Mar 10, 2026
3baa6b1
feat: context links for Slack-originated HTTP sessions, OAuth refresh…
AztecBot Mar 10, 2026
92fb9e9
feat: add elapsed time to status comments (Slack + GitHub)
AztecBot Mar 10, 2026
2cbc758
fix: ignore bot/app mentions, fix dashboard regex in template literal
AztecBot Mar 11, 2026
d8f388d
feat: direct redis read_log, build tools with full make targets, form…
AztecBot Mar 11, 2026
3c60433
feat: review profile, shared sidecar, webhook, session storage fix
AztecBot Mar 12, 2026
af39865
fix: add explicit permissions to test workflow
AztecBot Mar 12, 2026
c7c1aa6
Potential fix for code scanning alert no. 34: Clear-text logging of s…
ludamad Mar 12, 2026
28262f1
fix: remove MCP build tools, replace with Bash/make advice in CLAUDE.md
AztecBot Mar 12, 2026
e16bb2a
fix: kill leftover containers from previous runs before starting new …
AztecBot Mar 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
Expand Down
52 changes: 52 additions & 0 deletions bin/claudebox
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# ClaudeBox launcher — runs the server with auto-update restart loop.
#
# Usage:
# claudebox [--http-only] [--no-auto-update] [--profiles=X,Y] [...]
#
# The server exits with code 75 when auto-update pulls new code.
# This script restarts it automatically.

set -euo pipefail

# Resolve the repo directory (this script lives in <repo>/bin/)
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# Source env file if it exists (secrets, config)
ENV_FILE="$HOME/.claudebox/env"
if [ -f "$ENV_FILE" ]; then
set -a
source "$ENV_FILE"
set +a
fi

# Default to auto-update unless explicitly disabled
AUTO_UPDATE="--auto-update"
ARGS=()
for arg in "$@"; do
if [ "$arg" = "--no-auto-update" ]; then
AUTO_UPDATE=""
else
ARGS+=("$arg")
fi
done

cd "$REPO_DIR"

while true; do
echo "─── claudebox starting ($(git rev-parse --short HEAD 2>/dev/null || echo '?')) ───"
set +e
node --experimental-strip-types --no-warnings server.ts $AUTO_UPDATE "${ARGS[@]}"
EXIT_CODE=$?
set -e

if [ "$EXIT_CODE" = "75" ]; then
echo "─── auto-update: restarting ───"
sleep 1
continue
fi

echo "─── claudebox exited ($EXIT_CODE) ───"
exit $EXIT_CODE
done
13 changes: 13 additions & 0 deletions bin/refresh-oauth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Refresh OAuth token by briefly launching claude in a PTY
# script -c allocates a pseudo-TTY so claude thinks it's interactive

export HOME=/mnt/user-data/claude
export PATH="$HOME/.local/bin:$PATH"

script -qec "claude" /dev/null &
PID=$!
sleep 10
kill $PID 2>/dev/null
wait $PID 2>/dev/null
echo "[$(date -Is)] OAuth token refreshed (pid=$PID)"
14 changes: 14 additions & 0 deletions bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Bootstrap ClaudeBox — install dependencies.
# Called by install.sh and by auto-update after pulling new code.
set -euo pipefail

cd "$(dirname "$0")"

# Install/update npm deps (skip if node_modules is fresh)
if [ package.json -nt node_modules/.package-lock.json ] 2>/dev/null || [ ! -d node_modules ]; then
echo "[bootstrap] npm install..."
npm install --no-audit --no-fund --prefer-offline 2>&1 | tail -3
else
echo "[bootstrap] Dependencies up to date."
fi
192 changes: 192 additions & 0 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,193 @@ async function streamLogs(server: { url: string; user: string; password: string
}
}

// ── Creds command ────────────────────────────────────────────────

const CREDS_DIR = join(homedir(), ".claudebox", "credentials");
const CLAUDE_DIR = join(homedir(), ".claude");
const CLAUDE_CREDS = join(CLAUDE_DIR, ".credentials.json");

function credsSlots(): { name: string; path: string }[] {
if (!existsSync(CREDS_DIR)) return [];
return readdirSync(CREDS_DIR)
.filter(d => existsSync(join(CREDS_DIR, d, ".credentials.json")))
.sort()
.map(name => ({ name, path: join(CREDS_DIR, name) }));
}

function readCredsMeta(credPath: string): { email: string; expires: string; expired: boolean } {
try {
const creds = JSON.parse(readFileSync(join(credPath, ".credentials.json"), "utf-8"));
const oauth = creds.claudeAiOauth || {};
const expiresAt = oauth.expiresAt || 0;
const expired = expiresAt < Date.now();
const expiresDate = expiresAt ? new Date(expiresAt).toISOString().replace("T", " ").slice(0, 19) : "unknown";

// Try to read email from .claude.json in the slot
let email = "";
const claudeJson = join(credPath, ".claude.json");
if (existsSync(claudeJson)) {
try {
const cfg = JSON.parse(readFileSync(claudeJson, "utf-8"));
email = cfg.oauthAccount?.emailAddress || "";
} catch {}
}
// Fallback: check the token prefix for differentiation
if (!email) {
const token = (oauth.accessToken || "").slice(0, 25);
email = token ? `token:${token}...` : "unknown";
}
return { email, expires: expiresDate, expired };
} catch {
return { email: "error", expires: "error", expired: true };
}
}

function activeSlotName(): string | null {
if (!existsSync(CLAUDE_CREDS)) return null;
try {
const activeCreds = readFileSync(CLAUDE_CREDS, "utf-8");
const activeToken = JSON.parse(activeCreds).claudeAiOauth?.refreshToken || "";
if (!activeToken) return null;
for (const slot of credsSlots()) {
try {
const slotCreds = JSON.parse(readFileSync(join(slot.path, ".credentials.json"), "utf-8"));
if ((slotCreds.claudeAiOauth?.refreshToken || "") === activeToken) return slot.name;
} catch {}
}
} catch {}
return null;
}

async function credsCommand(args: string[]): Promise<void> {
const sub = args[0];

if (sub === "list" || sub === "ls" || !sub) {
const slots = credsSlots();
if (!slots.length) {
console.log(`No credential slots found.\n\nSet up slots:\n mkdir -p ${CREDS_DIR}/account-1\n cp ~/.claude/.credentials.json ${CREDS_DIR}/account-1/`);
return;
}
const active = activeSlotName();
const pad = Math.max(...slots.map(s => s.name.length), 4);

console.log(`\n ${"SLOT".padEnd(pad)} ${"IDENTITY".padEnd(30)} ${"EXPIRES".padEnd(19)} STATUS`);
console.log(` ${"─".repeat(pad)} ${"─".repeat(30)} ${"─".repeat(19)} ──────`);
for (const slot of slots) {
const meta = readCredsMeta(slot.path);
const isActive = slot.name === active;
const marker = isActive ? "● " : " ";
const statusParts: string[] = [];
if (isActive) statusParts.push("\x1b[32mactive\x1b[0m");
if (meta.expired) statusParts.push("\x1b[31mexpired\x1b[0m");
else statusParts.push("\x1b[32mvalid\x1b[0m");
console.log(`${marker}${slot.name.padEnd(pad)} ${meta.email.padEnd(30)} ${meta.expires} ${statusParts.join(" ")}`);
}
console.log();
return;
}

if (sub === "use" || sub === "promote") {
const slotName = args[1];
if (!slotName) { console.error("Usage: claudebox creds use <slot-name>"); process.exit(1); }

const slot = credsSlots().find(s => s.name === slotName);
if (!slot) { console.error(`Slot "${slotName}" not found. Run: claudebox creds list`); process.exit(1); }

const srcCreds = join(slot.path, ".credentials.json");
if (!existsSync(srcCreds)) { console.error(`No .credentials.json in slot "${slotName}"`); process.exit(1); }

// Back up current credentials to a slot if not already tracked
const active = activeSlotName();
if (active && active !== slotName) {
// Current creds are already in a slot, just switch
console.log(` Deactivating: ${active}`);
} else if (!active && existsSync(CLAUDE_CREDS)) {
// Current creds aren't in any slot — save them
const backupName = `backup-${Date.now()}`;
const backupDir = join(CREDS_DIR, backupName);
mkdirSync(backupDir, { recursive: true });
writeFileSync(join(backupDir, ".credentials.json"), readFileSync(CLAUDE_CREDS));
chmodSync(join(backupDir, ".credentials.json"), 0o600);
console.log(` Backed up current credentials → ${backupName}`);
}

// Copy slot credentials to active location
writeFileSync(CLAUDE_CREDS, readFileSync(srcCreds));
chmodSync(CLAUDE_CREDS, 0o600);

// Also copy .claude.json if the slot has one
const srcClaudeJson = join(slot.path, ".claude.json");
const dstClaudeJson = join(homedir(), ".claude.json");
if (existsSync(srcClaudeJson)) {
writeFileSync(dstClaudeJson, readFileSync(srcClaudeJson));
chmodSync(dstClaudeJson, 0o600);
}

const meta = readCredsMeta(slot.path);
console.log(` ● Activated: ${slotName} (${meta.email})`);
if (meta.expired) console.log(` ⚠ Token is expired — Claude will refresh on next use`);
console.log();
return;
}

if (sub === "save") {
const slotName = args[1];
if (!slotName) { console.error("Usage: claudebox creds save <slot-name>"); process.exit(1); }
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(slotName)) { console.error("Invalid slot name (use alphanumeric, hyphens, dots)"); process.exit(1); }

const slotDir = join(CREDS_DIR, slotName);
mkdirSync(slotDir, { recursive: true });

if (!existsSync(CLAUDE_CREDS)) { console.error("No active credentials to save"); process.exit(1); }

writeFileSync(join(slotDir, ".credentials.json"), readFileSync(CLAUDE_CREDS));
chmodSync(join(slotDir, ".credentials.json"), 0o600);

// Also save .claude.json for the account identity
const claudeJson = join(homedir(), ".claude.json");
if (existsSync(claudeJson)) {
writeFileSync(join(slotDir, ".claude.json"), readFileSync(claudeJson));
chmodSync(join(slotDir, ".claude.json"), 0o600);
}

const meta = readCredsMeta(slotDir);
console.log(` Saved current credentials → ${slotName}`);
console.log();
return;
}

if (sub === "rm" || sub === "remove") {
const slotName = args[1];
if (!slotName) { console.error("Usage: claudebox creds rm <slot-name>"); process.exit(1); }

const slot = credsSlots().find(s => s.name === slotName);
if (!slot) { console.error(`Slot "${slotName}" not found`); process.exit(1); }

const active = activeSlotName();
if (active === slotName) { console.error(`Cannot remove active slot. Switch first: claudebox creds use <other>`); process.exit(1); }

const { rmSync } = await import("fs");
rmSync(slot.path, { recursive: true });
console.log(` Removed slot: ${slotName}`);
console.log();
return;
}

console.log(`claudebox creds — manage Claude OAuth credentials

Usage:
claudebox creds List all credential slots
claudebox creds list List all credential slots
claudebox creds use <slot> Activate a credential slot
claudebox creds save <slot> Save current credentials to a slot
claudebox creds rm <slot> Remove a credential slot

Slots are stored in: ${CREDS_DIR}/
Each slot is a directory containing .credentials.json (and optionally .claude.json).
`);
}

// ── Main ────────────────────────────────────────────────────────

const [command, ...args] = process.argv.slice(2);
Expand Down Expand Up @@ -1774,6 +1961,10 @@ switch (command) {
case "register":
registerCommand(args).catch(e => { console.error(e.message); process.exit(1); });
break;
case "creds":
case "credentials":
credsCommand(args).catch(e => { console.error(e.message); process.exit(1); });
break;
default:
console.log(`ClaudeBox CLI

Expand All @@ -1795,6 +1986,7 @@ Usage:
claudebox register --user-id <id> --server-url <url> Register for DM routing
claudebox status Health check / local summary
claudebox profiles List available profiles
claudebox creds [list|use|save|rm] Manage OAuth credentials
claudebox config <key> [value] Get/set config

Sessions are identified by name (session/<name>) or worktree ID.
Expand Down
7 changes: 4 additions & 3 deletions container-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ cat > /tmp/mcp.json <<EOF
}
EOF

# ── Step 2: Write session metadata CLAUDE.md ─────────────────────
# Profile instructions live in $PROFILE_DIR/CLAUDE.md (loaded via --add-dir).
# This file just injects session-specific context into the workspace.
# ── Step 2: Write workspace CLAUDE.md (session metadata only) ──
# Profile instructions come via --add-dir $PROFILE_DIR; the env var
# CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 tells Claude to load
# CLAUDE.md from --add-dir directories.
mkdir -p /workspace/.claude
cat > /workspace/.claude/CLAUDE.md <<METAEOF
# Session
Expand Down
Loading
Loading